Skip to content

Commit

Permalink
fix/enhance(frontend): 映像・音声周りの改修 (#13206)
Browse files Browse the repository at this point in the history
* enhance(frontend): 映像・音声周りの改修

* fix

* fix design

* fix lint

* キーボードショートカットを整備

* Update Changelog

* fix

* feat: ループ再生

* ネイティブの動作と同期されるように

* Update Changelog

* key指定を消す
  • Loading branch information
kakkokari-gtyih authored Mar 30, 2024
1 parent 50da7d2 commit b96d9c6
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 19 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
- Enhance: ページのデザインを変更
- Enhance: 2要素認証(ワンタイムパスワード)の入力欄を改善
- Enhance: 「今日誕生日のフォロー中ユーザー」ウィジェットを手動でリロードできるように
- Enhance: 映像・音声の再生にブラウザのネイティブプレイヤーを使用できるように
- Enhance: 映像・音声の再生メニューに「再生速度」「ループ再生」「ピクチャインピクチャ」を追加
- Enhance: 映像・音声の再生にキーボードショートカットが使えるように
- Fix: 一部のページ内リンクが正しく動作しない問題を修正
- Fix: 周年の実績が閏年を考慮しない問題を修正
- Fix: ローカルURLのプレビューポップアップが左上に表示される
Expand Down
18 changes: 18 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4932,6 +4932,10 @@ export interface Locale extends ILocale {
* アプリを起動
*/
"launchApp": string;
/**
* 動画・音声の再生にブラウザのUIを使用する
*/
"useNativeUIForVideoAudioPlayer": string;
"_bubbleGame": {
/**
* 遊び方
Expand Down Expand Up @@ -9834,6 +9838,20 @@ export interface Locale extends ILocale {
*/
"summaryProxyDescription2": string;
};
"_mediaControls": {
/**
* ピクチャインピクチャ
*/
"pip": string;
/**
* 再生速度
*/
"playbackRate": string;
/**
* ループ再生
*/
"loop": string;
};
}
declare const locales: {
[lang: string]: Locale;
Expand Down
7 changes: 7 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,7 @@ notUsePleaseLeaveBlank: "使用しない場合は空欄にしてください"
useTotp: "ワンタイムパスワードを使う"
useBackupCode: "バックアップコードを使う"
launchApp: "アプリを起動"
useNativeUIForVideoAudioPlayer: "動画・音声の再生にブラウザのUIを使用する"

_bubbleGame:
howToPlay: "遊び方"
Expand Down Expand Up @@ -2619,3 +2620,9 @@ _urlPreviewSetting:
summaryProxy: "プレビューを生成するプロキシのエンドポイント"
summaryProxyDescription: "Misskey本体ではなく、サマリープロキシを使用してプレビューを生成します。"
summaryProxyDescription2: "プロキシには下記パラメータがクエリ文字列として連携されます。プロキシ側がこれらをサポートしない場合、設定値は無視されます。"

_mediaControls:
pip: "ピクチャインピクチャ"
playbackRate: "再生速度"
loop: "ループ再生"

112 changes: 111 additions & 1 deletion packages/frontend/src/components/MkMediaAudio.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only

<template>
<div
ref="playerEl"
v-hotkey="keymap"
tabindex="0"
:class="[
$style.audioContainer,
(audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
]"
@contextmenu.stop
@keydown.stop
>
<button v-if="hide" :class="$style.hidden" @click="hide = false">
<div :class="$style.hiddenTextWrapper">
Expand All @@ -18,6 +22,19 @@ SPDX-License-Identifier: AGPL-3.0-only
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
</div>
</button>

<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer">
<audio
ref="audioEl"
preload="metadata"
controls
:class="$style.nativeAudio"
@keydown.prevent
>
<source :src="audio.url">
</audio>
</div>

<div v-else :class="$style.audioControls">
<audio
ref="audioEl"
Expand Down Expand Up @@ -72,6 +89,41 @@ const props = defineProps<{
audio: Misskey.entities.DriveFile;
}>();

const keymap = {
'up': () => {
if (hasFocus() && audioEl.value) {
volume.value = Math.min(volume.value + 0.1, 1);
}
},
'down': () => {
if (hasFocus() && audioEl.value) {
volume.value = Math.max(volume.value - 0.1, 0);
}
},
'left': () => {
if (hasFocus() && audioEl.value) {
audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
}
},
'right': () => {
if (hasFocus() && audioEl.value) {
audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
}
},
'space': () => {
if (hasFocus()) {
togglePlayPause();
}
},
};

// PlayerElもしくはその子要素にフォーカスがあるかどうか
function hasFocus() {
if (!playerEl.value) return false;
return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
}

const playerEl = shallowRef<HTMLDivElement>();
const audioEl = shallowRef<HTMLAudioElement>();

// eslint-disable-next-line vue/no-setup-props-destructure
Expand All @@ -85,6 +137,30 @@ function showMenu(ev: MouseEvent) {

menu = [
// TODO: 再生キューに追加
{
type: 'switch',
text: i18n.ts._mediaControls.loop,
icon: 'ti ti-repeat',
ref: loop,
},
{
type: 'radio',
text: i18n.ts._mediaControls.playbackRate,
icon: 'ti ti-clock-play',
ref: speed,
options: {
'0.25x': 0.25,
'0.5x': 0.5,
'0.75x': 0.75,
'1.0x': 1,
'1.25x': 1.25,
'1.5x': 1.5,
'2.0x': 2,
},
},
{
type: 'divider',
},
{
text: i18n.ts.hide,
icon: 'ti ti-eye-off',
Expand Down Expand Up @@ -147,6 +223,8 @@ const rangePercent = computed({
},
});
const volume = ref(.25);
const speed = ref(1);
const loop = ref(false); // TODO: ドライブファイルのフラグに置き換える
const bufferedEnd = ref(0);
const bufferedDataRatio = computed(() => {
if (!audioEl.value) return 0;
Expand Down Expand Up @@ -176,6 +254,7 @@ function toggleMute() {
}

let onceInit = false;
let mediaTickFrameId: number | null = null;
let stopAudioElWatch: () => void;

function init() {
Expand All @@ -195,8 +274,12 @@ function init() {
}

elapsedTimeMs.value = audioEl.value.currentTime * 1000;

if (audioEl.value.loop !== loop.value) {
loop.value = audioEl.value.loop;
}
}
window.requestAnimationFrame(updateMediaTick);
mediaTickFrameId = window.requestAnimationFrame(updateMediaTick);
}

updateMediaTick();
Expand Down Expand Up @@ -234,6 +317,14 @@ watch(volume, (to) => {
if (audioEl.value) audioEl.value.volume = to;
});

watch(speed, (to) => {
if (audioEl.value) audioEl.value.playbackRate = to;
});

watch(loop, (to) => {
if (audioEl.value) audioEl.value.loop = to;
});

onMounted(() => {
init();
});
Expand All @@ -252,6 +343,10 @@ onDeactivated(() => {
hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore');
stopAudioElWatch();
onceInit = false;
if (mediaTickFrameId) {
window.cancelAnimationFrame(mediaTickFrameId);
mediaTickFrameId = null;
}
});
</script>

Expand All @@ -262,6 +357,10 @@ onDeactivated(() => {
border: .5px solid var(--divider);
border-radius: var(--radius);
overflow: clip;

&:focus {
outline: none;
}
}

.sensitive {
Expand Down Expand Up @@ -367,4 +466,15 @@ onDeactivated(() => {
}
}
}

.nativeAudioContainer {
display: flex;
align-items: center;
padding: 6px;
}

.nativeAudio {
display: block;
width: 100%;
}
</style>
Loading

0 comments on commit b96d9c6

Please sign in to comment.