From 8b0fdfcd69334dbf934a69cf707826b3be8cf2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 15 Jan 2024 18:17:01 +0900 Subject: [PATCH] =?UTF-8?q?enhance:=20=E5=8B=95=E7=94=BB=E3=83=BB=E9=9F=B3?= =?UTF-8?q?=E5=A3=B0=E5=91=A8=E3=82=8A=E3=81=AEUI=E3=81=A8=E5=8B=95?= =?UTF-8?q?=E4=BD=9C=E6=94=B9=E8=89=AF=20(#12925)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * (fix) `/files` をバイトレンジリクエストに対応させる * video * audio * fix * fix * spdx * fix (rangeRequest) * fix * Update CHANGELOG.md * (add) ボリュームを保存できるように * (fix) ミュート復帰時に音量が固定される * named export * tweak design * Add sensitive class for audio component * Refactor seekbar styles * Refactor hms * Revert "(add) ボリュームを保存できるように" This reverts commit 6271f9493b63f96d0dd9915207e97fe120ef9037. * Revert "(fix) ミュート復帰時に音量が固定される" This reverts commit a65002b56ecdcb10f76bcc2debbe38593a69643f. * revert revert changes --------- Co-authored-by: syuilo --- CHANGELOG.md | 2 + locales/index.d.ts | 2 + locales/ja-JP.yml | 2 + .../backend/src/server/FileServerService.ts | 111 +++- .../frontend/src/components/MkMediaAudio.vue | 363 ++++++++++++ .../frontend/src/components/MkMediaBanner.vue | 13 +- .../frontend/src/components/MkMediaRange.vue | 150 +++++ .../frontend/src/components/MkMediaVideo.vue | 540 ++++++++++++++++-- packages/frontend/src/filters/hms.ts | 65 +++ packages/frontend/src/scripts/device-kind.ts | 7 + 10 files changed, 1180 insertions(+), 75 deletions(-) create mode 100644 packages/frontend/src/components/MkMediaAudio.vue create mode 100644 packages/frontend/src/components/MkMediaRange.vue create mode 100644 packages/frontend/src/filters/hms.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f1fdcf9e..945b6ac1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### Client - Feat: 新しいゲームを追加 +- Feat: 音声・映像プレイヤーを追加 - Feat: 絵文字の詳細ダイアログを追加 - Feat: 枠線をつけるMFM`$[border.width=1,style=solid,color=fff,radius=0 ...]`を追加 - Enhance: ハッシュタグ入力時に、本文の末尾の行に何も書かれていない場合は新たにスペースを追加しないように @@ -38,6 +39,7 @@ - Enhance: 連合先のレートリミットに引っかかった際にリトライするようになりました - Enhance: ActivityPub Deliver queueでBodyを事前処理するように (#12916) - Enhance: クリップをエクスポートできるように +- Enhance: `/files`のファイルに対してHTTP Rangeリクエストを行えるように - Enhance: `api.json`のOpenAPI Specificationを3.1.0に更新 - Fix: `drive/files/update`でファイル名のバリデーションが機能していない問題を修正 - Fix: `notes/create`で、`text`が空白文字のみで構成されているか`null`であって、かつ`text`だけであるリクエストに対するレスポンスが400になるように変更 diff --git a/locales/index.d.ts b/locales/index.d.ts index dafbdd355..71134544d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1061,6 +1061,8 @@ export interface Locale { "noteIdOrUrl": string; "video": string; "videos": string; + "audio": string; + "audioFiles": string; "dataSaver": string; "accountMigration": string; "accountMoved": string; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 58952894b..743a3ca38 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1058,6 +1058,8 @@ limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小し noteIdOrUrl: "ノートIDまたはURL" video: "動画" videos: "動画" +audio: "音声" +audioFiles: "音声" dataSaver: "データセーバー" accountMigration: "アカウントの移行" accountMoved: "このユーザーは新しいアカウントに移行しました:" diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts index f59996ce1..7745a6cb7 100644 --- a/packages/backend/src/server/FileServerService.ts +++ b/packages/backend/src/server/FileServerService.ts @@ -168,11 +168,35 @@ export class FileServerService { } if (!image) { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + + image = { + data: fs.createReadStream(file.path, { + start, + end, + }), + ext: file.ext, + type: file.mime, + }; + + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + } else { + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; + } } if ('pipe' in image.data && typeof image.data.pipe === 'function') { @@ -203,11 +227,54 @@ export class FileServerService { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.mime) ? file.mime : 'application/octet-stream'); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', filename)); + + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + const fileStream = fs.createReadStream(file.path, { + start, + end, + }); + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + reply.code(206); + return fileStream; + } + return fs.createReadStream(file.path); } else { reply.header('Content-Type', FILE_TYPE_BROWSERSAFE.includes(file.file.type) ? file.file.type : 'application/octet-stream'); reply.header('Cache-Control', 'max-age=31536000, immutable'); reply.header('Content-Disposition', contentDisposition('inline', file.filename)); + + if (request.headers.range && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + console.log(end); + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + const fileStream = fs.createReadStream(file.path, { + start, + end, + }); + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + reply.code(206); + return fileStream; + } + return fs.createReadStream(file.path); } } catch (e) { @@ -340,11 +407,35 @@ export class FileServerService { } if (!image) { - image = { - data: fs.createReadStream(file.path), - ext: file.ext, - type: file.mime, - }; + if (request.headers.range && file.file && file.file.size > 0) { + const range = request.headers.range as string; + const parts = range.replace(/bytes=/, '').split('-'); + const start = parseInt(parts[0], 10); + let end = parts[1] ? parseInt(parts[1], 10) : file.file.size - 1; + if (end > file.file.size) { + end = file.file.size - 1; + } + const chunksize = end - start + 1; + + image = { + data: fs.createReadStream(file.path, { + start, + end, + }), + ext: file.ext, + type: file.mime, + }; + + reply.header('Content-Range', `bytes ${start}-${end}/${file.file.size}`); + reply.header('Accept-Ranges', 'bytes'); + reply.header('Content-Length', chunksize); + } else { + image = { + data: fs.createReadStream(file.path), + ext: file.ext, + type: file.mime, + }; + } } if ('cleanup' in file) { diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue new file mode 100644 index 000000000..75b31b9a4 --- /dev/null +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -0,0 +1,363 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue index 3f8fef663..b21960a49 100644 --- a/packages/frontend/src/components/MkMediaBanner.vue +++ b/packages/frontend/src/components/MkMediaBanner.vue @@ -5,20 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only