Slide 1

Slide 1 text

ブラウザ単体で mp4書き出すまで pixiv Inc. yue 2023.4.11

Slide 2

Slide 2 text

2 基本情報 ● 本名: 王 越(yue / ユエ) ● 2021年より3D事業部/VRoid部所属 ● https://yue.cat yue エンジニア

Slide 3

Slide 3 text

3 1. VRoid / VRoid Hub紹介 2. 実装になった経緯 3. 仕様と実装のバランス a. ブラウザの制限 b. デバイスの制限 4. リリース後のトラブル・修正 内容

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

6

Slide 7

Slide 7 text

7 なぜブラウザからmp4を書き出したい? 
 
 実装になった経緯 


Slide 8

Slide 8 text

8 VRM(3Dモデル)のアニメーション記録ファイルの仕様である 
 VRM Animation(VRMA) 正式版の仕様リリースが決まり、 
 リリース日にVRMAを試せる場所が欲しい 
 
 前々からやりたいね〜みたいな機運もあった


Slide 9

Slide 9 text

9

Slide 10

Slide 10 text

10 アニメーションなので動画は共有したいよね!!! 
 


Slide 11

Slide 11 text

11 要件整理1 : 出力フォーマット ● 静的な画像以外、動くものも欲しい ○ gif / 動画問わず ○ 実現可能性は早い段階で確かめたい ● SNSでの共有しやすさ重視 ○ 一回撮影したものの編集も考えたい ● スピード感重視 ○ リリースまで~2月

Slide 12

Slide 12 text

12 選択肢 ● サーバーサイドエンコード ● Animated AVIF ● Animated PNG ● GIF ● WebM ● MP4 ● ..

Slide 13

Slide 13 text

13 選択肢ではなかった ● サーバーサイドエンコード ○ サーバーを用意するのに時間がかかりすぎて間に合わない ○ 転送するデータの容量もデカいので UXが良くない ○ お金もかかり続ける ● Animated AVIF ○ encodingコストが高い ○ 認知しているユーザーが少ない ● Animated PNG ○ 投稿として対応しているSNSが少なく、最終的に検証も実装されなかった

Slide 14

Slide 14 text

14 試したが辞めた ● GIF ○ 最初からこのアプローチを取って実装を進めたが、 X(Twitter)の容量制限にす ぐに引っかかってしまうことがわかり、一度実装したが捨てた ■ 10秒くらいの動画でも共有できるか怪しくシェア目的に向いてない ○ この時点でできるだけ動画にしたい方向にした

Slide 15

Slide 15 text

15 ティア表 ● 共有しやすさ ○ mp4 > gif > webm > av1f / apng ● 実装難易度 ○ webm > gif > mp4 > av1f / apng ● 編集しやすさ ○ mp4 > webm > gif / av1f / apng

Slide 16

Slide 16 text

16 仕様と実装のバランス 
 
 ブラウザの制限 


Slide 17

Slide 17 text

17 MP4 / WebM ● ffmpeg-wasm ○ 社内からffmpeg-wasmでH.264を使うと特許とライセンスの問題に遭遇する可 能性がある指摘を受けたため辞めた ● MediaRecorder / Canvas.captureStream() ○ こちらを試すことに

Slide 18

Slide 18 text

18 MediaRecorder / Canvas.captureStream ● MediaRecorderはリアルタイムの記録であるため、フレーム落ちが発生するとフ レーム落ちしている様子も記録されてしまう懸念があった ● CanvasCaptureMediaStreamTrack.requestFrame() でフレームを制御すれば この問題は回避できる事がわかった ○ ただしFirefoxではこのメソッドは未実装 ● MediaRecorderではブラウザ毎対応しているコーデックがばらばらで、 Xに直接シェ ア出来るのはmp4書き出せるSafariだけだった ○ Safari→mp4 ○ Chrome→WebM(VP8/9) ○ Firefox→WebM(VP8)

Slide 19

Slide 19 text

19 MediaRecorder / Canvas.captureStream const videoType = [ // mimeType, ext ['video/mp4', 'mp4'], // Safari Only ['video/webm;codecs="vp9"', 'webm'], ['video/webm;codecs="vp8"', 'webm'], ].find((type) => MediaRecorder.isTypeSupported(type[0]));

Slide 20

Slide 20 text

20 chromeのmp4書き出し対応を探しに 
 さらに森の奥へ 


Slide 21

Slide 21 text

21 その他の実装を試す ● recordrtc ○ unpkg.com上のスクリプトの実行をContent-Security-Policyで許可する必要 があった ■ それはできれば避けたかったので最終手段にした ● mattdesl/mp4-wasm ○ WebCodecs APIから吐き出されたチャンクをMP4にmux出来そうだったので試 した ■ 出来た ■ しかしWindowsだけ特定な処理がクラッシュしてしまうことが発覚 ● wasmの中でプラットフォーム固有な問題に遭 遇するのが初めて

Slide 22

Slide 22 text

22 その他の実装を試す ● canvas-record ○ WebCodecs経由で書き出して、別途MP4に詰める ○ FirefoxはWebCodecs APIが未実装でMP4を書き出せる手段がないため、 WebM書き出しのままリリースすることになった editorialViewer.startRecording(async () => { captureFrameToBuffer(); await recorder.step(); incrRecordingFrames(); });

Slide 23

Slide 23 text

23 仕様整理1: 出力フォーマット このような対応表になった ● Chrome / Edge ○ 仕組み: WebCodecs + canvas-record ○ フォーマット: mp4 ● Safari ○ 仕組み: MediaRecorder ○ フォーマット: mp4 ● Firefox ○ 仕組み: MediaRecorder ○ フォーマット: webm (vp9 or vp8 / vp9優先)

Slide 24

Slide 24 text

24 仕様と実装のバランス 
 
 デバイスの制限


Slide 25

Slide 25 text

25 要件整理2: 出力サイズ ● 固定アスペクト比の出力が欲しい ○ 16:9 ○ 9:16 ● 固定サイズの出力が欲しい ○ X(Twitter)サムネ 1200x630 ○ VRoidHubサムネ 900x1200 ● できるだけ出力クオリティを保ちたい

Slide 26

Slide 26 text

26 実装上のチャレンジ ● 様々なサイズ ○ 枠のサイズ ■ = 親Elementのcss上のサイズ ○ 表示サイズ ■ = 親の中でアスペクト比適用後css上のサイズ ○ レンダリングサイズ ■ DPR(device pixel ratio)によって表示サイズとレンダリングサイズ実は 違う

Slide 27

Slide 27 text

27 DPRあり・なし

Slide 28

Slide 28 text

28 実装上のチャレンジ ● アスペクト比の固定 ○ object-fitのjs側実装が必要 ○ → object-fit-math ■ fitAndPosition(canvas, output) ● 出力サイズ ○ ユーザーの設定によって最終的に計算されるもの ○ DPRを考慮したい

Slide 29

Slide 29 text

29 出力サイズ決め // 枠のサイズそのままの場合、表示エリアと WebGLの仕様をケアする const outputResolution = { // 出力サイズ width: nearEvenize(viewerRect.width // 枠のサイズ), // 奇数を避ける height: nearEvenize(viewerRect.height) };  // 枠内のレンダリングサイズ const renderPosition = fitAndPosition(viewerRect, outputResolution, 'contain'); renderPosition.width = Math.ceil(renderPosition.width); // 少数を避ける renderPosition.height = Math.ceil(renderPosition.height); renderPosition.x = Math.ceil(renderPosition.x); renderPosition.y = Math.ceil(renderPosition.y);

Slide 30

Slide 30 text

30 PCで動いた! 🎉
 が、モバイルでクラッシュする..


Slide 31

Slide 31 text

31 devicePixelRatio 大体1 or 2 をイメージする人が多いが、yue手元のデバイスでは.. https://ebisu.com/size/ iPhone 13mini 某Androidデバイス

Slide 32

Slide 32 text

32 1920x1080 * 3 = 5760x3240 !
 
 もちろん静画ですらレンダリングするのが難しい
 
 => いい感じにする


Slide 33

Slide 33 text

33 いい感じにする // try render 2x if needed for better quality but avoid dpr over 2 which is too large for mobile to render if (isMobile) { const dprLimit = Math.min(window.devicePixelRatio, 2); outputResolution.width = nearEvenize(renderPosition.width * dprLimit); outputResolution.height = nearEvenize(renderPosition.height * dprLimit); } アスペクト比を維持した状態の表示サイズ * min(dpr, 2) dprが2超えた場合を2にする

Slide 34

Slide 34 text

34 仕様整理2: 出力サイズ ● PC ○ 表示サイズ=1000x1000, 16:9を選択, dpr=2 ■ 出力サイズ ⇒ 1920×1080 * 2 = 3840×2160 ○ 表示サイズ=1000x1000, 1:1を選択, dpr=2 ■ 出力サイズ ⇒ 1080×1080 * 2 = 2160×2160 ● モバイル ○ 表示サイズ=160x160, 16:9を選択, dpr=3 ■ 出力サイズ ⇒ 160x90 * min(3, 2) = 320x180 ○ 表示サイズ=300x200, 1:1を選択, dpr=1 ■ 出力サイズ ⇒ 200x200 * min(1, 2) = 200x200 ※将来的に調整される可能性は あります

Slide 35

Slide 35 text

35 実装上の一部のエッジケース ● 動画撮影中ページ遷移が発生した ● 動画撮影中windowがリサイズされる ● …

Slide 36

Slide 36 text

おまけ・細かい対応 ● 背景対応 ● 前景対応 ● 透過 ● 文字解像度

Slide 37

Slide 37 text

おまけ WebGL Canvas内のレスポンシブ対応

Slide 38

Slide 38 text

38 ここまで PC/モバイル 両方 x 殆どのブラウザで mp4出力ができた 🎉


Slide 39

Slide 39 text

39 リリース後のトラブル・修正

Slide 40

Slide 40 text

40 以下のコード覚えてますか? const videoType = [ // mimeType, ext ['video/mp4', 'mp4'], // Safari Only ['video/webm;codecs="vp9"', 'webm'], ['video/webm;codecs="vp8"', 'webm'], ].find((type) => MediaRecorder.isTypeSupported(type[0]));

Slide 41

Slide 41 text

41 ● MediaRecorderではブラウザ毎対応しているコーデックがばらばらで、 Xに直接シェア出来 るのはSafariだけだった ○ Safari→mp4 ○ Chrome→WebM(VP8/9) ○ Firefox→WebM(VP8) と説明したが、 chrome v126から MediaRecorder.isTypeSupported('video/mp4') がtrueになったため、 分岐ロジックが壊れた。 https://chromestatus.com/feature/5163469011943424 トラブル1

Slide 42

Slide 42 text

なぜかaxiosをuninstallすると撮影が失敗する ● 経路として canvas-record -> h264-mp4-encoder -> embuild/dist/h264-mp4-encoder.web.js でビルドされたnodeのbufferモジュールが読み 込まれる ○ h264-mp4-encoderの中身はembuild経由でビルドされているため、簡単に patchで きなかったので一旦戻した。引き続き対応中。 トラブル2

Slide 43

Slide 43 text

43 制限と向き合い、色々面白いことやったり 最大限要件に答えた実装ができたと思う

Slide 44

Slide 44 text

44 終わり ご清聴ありがとうございました

Slide 45

Slide 45 text

45