LoginSignup
0
0

Meta QuestのExoPlayerを使って60fpsの動画をなめらかに再生する

Last updated at Posted at 2024-02-28

はじめに

3年ほど前に私が制作しているVR動画プレイヤーで実施した「ExoPlayerで動画をなめらかに再生する対策」について解説します。

問題定義

検証動画

この動画はMeta QuestのSDKに含まれる(現在はSDKとは別に配布されている)サンプルプログラムを使って60fpsの動画を再生した様子をレンズ越しに秒間480コマのハイスピードカメラで撮影したものです。
Quest 2でリフレッシュレートを60Hzにして撮影しています。
動画にはフレームカウンターが映っているので正常であれば映っているカウンターに重複や欠番はないはずですが・・・

解析結果

動画からカウンター部分を切り取ったのが次の画像です。左上から下に向かって進んでいきます。
CIMG3991_quest_original_60Hz_60fps.png

386フレームから867フレームまでの482フレーム(約1秒間に相当)に注目すると以下となります。

ハイスピードカメラに写っているフレームカウンターの値 xは重複
3080
3080x
3081
3083
3083x
3084
3086
3086x
3087
3089
3089x
3091
3091x
3092
3094
3094x
3095
3097
3097x
3098
3100
3100x
3101
3103
3103x
3104
3106
3106x
3107
3109
3110
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140

欠落番号(写っていない番号)
3082
3085
3088
3090
3093
3096
3099
3102
3105
3108

前半は10の重複フレームと、それに伴う10の欠落フレームがあります。
後半は重複も欠落もなくうまく再生できています。
このようにExoPlayerで60Hz、60fpsの条件だとうまく再生できたり不安定になったりを周期的に繰り返します。

今回はこれを改善し きっちり再生する方法を このサンプルプログラムを例に解説します。

環境

  • Unity 2022.3.10f1
  • Meta Quest 2/3

対策方針

ディスプレイの動作タイミングに合わせてExoPlayerの出力処理(processOutputBuffer())を行うようにする

具体的にはExoPlayerの出力処理の中でスレッドをロックし「待つ」仕組みを追加します。
そして、Unityのメインスレッドから1フレーム期間に一回だけExoPlayerのロックを解除します。

厳密には「ディスプレイの動作=メインスレッドの動作」ではないですが、これで60fpsの滑らかな再生が可能になります。
ハイスピードカメラを使わなくてもサンプル動画のスクロールの滑らかさで60fpsの再生が出来ているかどうか判断できます。

ダウンロード

対策内容

  1. プロジェクトの作成とサンプルプログラムの実行
  2. ExoPlayerの利用
  3. ExoPlayerをカスタマイズする

1と2はQuestでExoPlayerを使えるようにするための準備です。アプリ開発の経験があり対策だけ知りたい方は3を参照してください。

1.プロジェクトの作成とサンプルプログラムの実行

  1. プロジェクト作成
    Unity 2022.3.10f1「3D」テンプレートを使って新しいプロジェクトを作成します。
    new project.png

  2. サンプルプログラム
    Meta Questのサンプルプログラムをダウンロードして解凍してできるファイルを丸ごとプロジェクトへ上書きコピーします。
    file_copy.png

  3. とりあえず実行
    Assets\StarterSamples\UsageにあるStereo180Videoをダブルクリックしてシーンを読み込む。
    stereo180video_prefab.png
    メニューのファイルにあるBuild Settingsから「Android」を選択して「Switch Platform」ボタンを押す。
    build setting.png
    ビルド&RUNでとりあえず実行できます。
    build_and_run.png
    再生する動画が無いのでただの真っ黒な画面になります。
    run.pngAボタンでコンソールが表示されます。
    ホームボタンから終了してください。

  4. 動画を再生する
    krkr.zipをダウンロード
    解凍してできるmp4ファイルを全てQuestのMoviesフォルダへ転送
    krkr.png

  5. サンプルプログラムを編集
    Stereo180Videoのメインとなるスクリプト「MoviePlayerSample.cs」を編集していきます。
    Assets\StarterSamples\Core\Video\ScriptsにあるMoviePlayerSample.csを開く。
    MoviePlayerSample_cs.png

    • 編集1
      初めの方にusing UnityEngine.UIを追加

      MoviePlayerSample.cs 24行目当たり
      using UnityEngine;
      using System;
      using System.IO;
      using UnityEngine.UI; // 追加
      
    • 編集2
      void Awake()の最後へMySetup();を追加

      MoviePlayerSample.cs 95行目当たり
          #if UNITY_EDITOR
              overlay.currentOverlayShape = OVROverlay.OverlayShape.Quad;
              overlay.enabled = true;
          #endif
          MySetup(); // 追加
      
    • 編集3
      private System.Collections.IEnumerator Start() の処理を行わないようにする

      MoviePlayerSample.cs 194行目当たり
      private System.Collections.IEnumerator Start()
      {
          yield break; // 追加 以降の処理(動画読み込み)を行わないようにする
          if (mediaRenderer.material == null)
      
    • 編集4
      void Update()の最後へMyUpdate();を追加
      リフレッシュレートの制御をしている個所をコメントアウト

      MoviePlayerSample.cs 342行目当たり
          else
          {
              NativeVideoPlayer.SetListenerRotation(Camera.main.transform.rotation);
              IsPlaying = NativeVideoPlayer.IsPlaying;
              PlaybackPosition = NativeVideoPlayer.PlaybackPosition;
              Duration = NativeVideoPlayer.Duration;
              
              // コメントアウト リフレッシュレートの制御
              //if (IsPlaying && (int)OVRManager.display.displayFrequency != 60)
              //{
              // OVRManager.display.displayFrequency = 60.0f;
              //}
              //else if (!IsPlaying && (int)OVRManager.display.displayFrequency != 72)
              //{
              // OVRManager.display.displayFrequency = 72.0f;
              //}
              // コメントアウト リフレッシュレートの制御
          }
          MyUpdate(); // 追加
      
    • 編集5
      class MoviePlayerSampleの最後へMySetup()とMyUpdate()を実装

      MoviePlayerSample.cs 416行目当たり
      private Text debug_text;
      private string[] video_list = {
          "krkr_60fps.mp4",
          "krkr_72fps.mp4"};
      private int video_idx = 0;
      
      private void MySetup()
      {
          GameObject mp = GameObject.Find("MoviePlayer");
          mp.transform.position = new Vector3(0, 0, 30);
          mp.transform.localScale = new Vector3(32f, 18f, 1f);
          
          GameObject canvas = GameObject.Find("Canvas");
          GameObject debug = new GameObject("debug_text");
          debug.transform.parent = canvas.transform;
          debug_text = debug.AddComponent<Text>();
          debug.transform.localPosition = Vector3.zero;
          debug.transform.localScale = Vector3.one;
          debug.GetComponent<RectTransform>().sizeDelta = new Vector2(1000, 200);
          debug.layer = canvas.layer;
          debug_text.font = Resources.GetBuiltinResource(typeof(Font), "Arial.ttf") as Font;
          debug_text.fontSize = 32;
          debug_text.color = new Color(1f, 0f, 0.8f);
          
          MovieName = "/storage/emulated/0/Movies/" + video_list[video_idx];
          Shape = VideoShape.Quad;
          Stereo = VideoStereo.Mono;
          Play(MovieName, DrmLicenseUrl);
      }
      
      private void MyUpdate()
      {
          if (OVRInput.GetDown(OVRInput.Button.Two)) OVRManager.display.displayFrequency = (OVRManager.display.displayFrequency == 60.0f) ? 72.0f : 60.0f;
          if (OVRInput.GetDown(OVRInput.Button.PrimaryHandTrigger))
          {
              video_idx++;
              if (video_idx > video_list.Length) video_idx = 0;
              MovieName = "/storage/emulated/0/Movies/" + video_list[video_idx];
              Play(MovieName, DrmLicenseUrl);
          }
          string info = overlay.isExternalSurface ? "Playing ExoPlayer with SurfaceObject" : "Playing Unity VideoPlayer";
          debug_text.text =
              $"{info}\n" +
              $"{MovieName}\n" +
              $"Display={OVRManager.display.displayFrequency:0} Hz\n" +
              $"";
          //NativeVideoPlayer.onRenderEvent();
      }
      
  6. Project Settings
    MoviesフォルダへアクセスするためにProject Settingsで内蔵ストレージへのアクセス権を要求するようにする
    project_settings_write_permission.png

  7. ビルドして実行
    krkr_60fps.mp4が再生されます。
    run_unity_player.png
    この段階ではUnityのプレイヤー機能を使って再生されます。
    Bボタンでディスプレイのリフレッシュレートが切り替わります。
    グリップボタンを押すごとに再生する動画が切り替わります。

    • krkr_60fps.mp4を再生して60Hzにすると なめらかに再生されます
    • krkr_72fps.mp4を再生して72Hzにすると なめらかに再生されます
      ※ Quest3の場合は 60Hzにできません。

    サンプルプログラムには2つのシナリオがあり一つはUnityのプレイヤー機能を使い、もう一つはExoPlayerを使うようになっていてoverlay.isExternalSurfaceで選択されます。
    Unity EditorやOculus Riftで実行する場合はUnityのプレイヤー機能で再生するようになっています(ExoPlayerはAndroidの機能なのでPCでは使えない)。
    Questの実機で実行した場合でもExoPlayerの準備ができていない場合はUnityのプレイヤー機能を使って動作するようになっています。
    :

2.ExoPlayerを利用する

  1. ExoPlayerを有効にする
    UnityのメニューにあるOculusから「Enable Naitive Android Video Player」を有効にします。
    enable_native_android_video_player.png

  2. 各設定
    以下の設定をしてください。

    • Project SettingsでTarget API Levelを「Android 12.0 (API level 31)」にする(既になっている)

    • 以下の3つのファイルのプロパティでAndroidをOnにしてApplyボタンを押す
      Assets\StarterSamples\Core\Video\Plugins\Android\Audio360にある

      • audio360.aar
      • audio360-exo218.aar

      Assets\StarterSamples\Core\Video\Plugins\Android\java\com\oculus\videoplayerにある

      • NativeVideoPlayer.java
        native_video_player_prop.png
    • ExoPlayerのバージョンを2.18.1にする
      Assets\Plugins\AndroidにあるmainTemplate.gradleを開いて編集します。

      mainTemplate.gradle
      apply plugin: 'com.android.library'
      **APPLY_PLUGINS**
      
      dependencies {
      implementation fileTree(dir: 'libs', include: ['*.jar'])
      implementation 'com.google.android.exoplayer:exoplayer:2.18.1'
      
  3. ビルドして実行
    Playing ExoPlayerとなっていればExoPlayerで再生できています。
    run_playing_exoplayer.png
    ただし、Unityのプレヤー機能と比べ 時々カクカクするのが解ると思います(これが本題)。

Unityのプレイヤー機能の方が滑らかなのでUnityのプレイヤー機能を使えば良くない?
恐らくUnityのプレイヤー機能はゲームのオープニングとか必殺技の前のカットイン動画のように制作時にあらかじめ用意した動画を目的としたものだとおもいます。
メディアプレイヤーの様に利用者が用意した動画やネットワーク再生等になると再生が途切れたり、読み込み、シークなどで待たされる場面があります。

3.ExoPlayerをカスタマイズする

以下の3つのスクリプトを編集します。

  1. メインスクリプト
    Assets\StarterSamples\Core\Video\Scriptsにある
    MoviePlayerSample.cs

    onRenderEvent()のコメントアウトを外す

    MoviePlayerSample.cs 467行目あたり
    NativeVideoPlayer.onRenderEvent(); // コメントアウトを外す
    

    onRenderEvent()関数をUpdate()の中で呼び出すことで 1フレーム期間に一回だけExoPlayerのスレッドのロックを解除します。

  2. ネイティブプレイヤースクリプト
    Assets\StarterSamples\Core\Video\Plugins\Androidにある
    NativeVideoPlayer.cs

    このクラスはUnityのメインスクリプトとExoPlayerのjavaスクリプトの間を取り持つクラスです。
    クラスの最後にある public static void SetListenerRotation()の後にonRenderEvent()を実装します。

    NativeVideoPlayer.cs SetListenerRotation()の後
    private static System.IntPtr onRenderEventMethodId;
    public static void onRenderEvent()
    {
        if (onRenderEventMethodId == System.IntPtr.Zero)
        {
            onRenderEventMethodId = AndroidJNI.GetStaticMethodID(VideoPlayerClass, "onRenderEvent", "()V");
        }       
        AndroidJNI.CallStaticVoidMethod(VideoPlayerClass, onRenderEventMethodId, EmptyParams);
    }
    
  3. javaスクリプト
    Assets\StarterSamples\Core\Video\Plugins\Android\java\com\oculus\videoplayerにある
    NativeVideoPlayer.java

    これがExoPlayerのコア処理になります。

    • 編集1
      importを追加する

      NativeVideoPlayer.java 74行目あたり
      import com.google.android.exoplayer2.util.Assertions;
      import com.google.android.exoplayer2.C;
      import com.google.android.exoplayer2.ExoPlaybackException;
      import com.google.android.exoplayer2.mediacodec.MediaCodecSelector;
      import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter;
      import com.google.android.exoplayer2.video.MediaCodecVideoRenderer;
      import com.google.android.exoplayer2.video.VideoFrameReleaseHelper;
      import java.lang.reflect.Constructor;
      import java.nio.ByteBuffer;
      import android.os.SystemClock;
      
    • 編集2
      メインスクリプトから呼び出すonRenderEvent()および各カスタムクラスを実装する

      NativeVideoPlayer.java クラスの最後 setPlaybackPosition()の後
      private static Object lock = new Object();
      public static void onRenderEvent()
      {
          synchronized (lock) {
              lock.notifyAll();
          }
      }
      
      // exoplayer ver 2.18.1
      private static class CustomMediaCodecVideoRenderer extends MediaCodecVideoRenderer
      {
          public CustomMediaCodecVideoRenderer(
              Context context,
              MediaCodecAdapter.Factory codecAdapterFactory,
              MediaCodecSelector mediaCodecSelector,
              long allowedJoiningTimeMs,
              boolean enableDecoderFallback,
              Handler eventHandler,
              VideoRendererEventListener eventListener,
              int maxDroppedFramesToNotify) {
          
              super(
                  context,
                  codecAdapterFactory,
                  mediaCodecSelector,
                  allowedJoiningTimeMs,
                  enableDecoderFallback,
                  eventHandler,
                  eventListener,
                  maxDroppedFramesToNotify,
                  30
                  );
              frameReleaseHelper = new VideoFrameReleaseHelper(context);
          }
          
          private final VideoFrameReleaseHelper frameReleaseHelper;
          
          
          @Override
          protected boolean processOutputBuffer(
              long positionUs,
              long elapsedRealtimeUs,
              MediaCodecAdapter codec,
              ByteBuffer buffer,
              int bufferIndex,
              int bufferFlags,
              int sampleCount,
              long bufferPresentationTimeUs,
              boolean isDecodeOnlyBuffer,
              boolean isLastBuffer,
              Format format)
              throws ExoPlaybackException {
          
              double playbackSpeed = getPlaybackSpeed();
              boolean isStarted = getState() == STATE_STARTED;
              long elapsedRealtimeNowUs = SystemClock.elapsedRealtime() * 1000;
              long earlyUs = (long) ((bufferPresentationTimeUs - positionUs) / playbackSpeed);
              if (isStarted) {
                  // Account for the elapsed time since the start of this iteration of the rendering loop.
                  // earlyUs -= elapsedRealtimeNowUs - elapsedRealtimeUs;
              }
          
              long outputStreamOffsetUs = getOutputStreamOffsetUs();
              long presentationTimeUs = bufferPresentationTimeUs - outputStreamOffsetUs;
              
              // Skip frames in sync with playback, so we'll be at the right frame if the mode changes.
              if (isBufferLate(earlyUs)) {
                  skipOutputBuffer(codec, bufferIndex, presentationTimeUs);
                  updateVideoFrameProcessingOffsetCounters(earlyUs);
                  return true;
              }
              
              // Compute the buffer's desired release time in nanoseconds.
              long systemTimeNs = System.nanoTime();
              long unadjustedFrameReleaseTimeNs = systemTimeNs + (earlyUs * 1000);
              
              long adjustedReleaseTimeNs = frameReleaseHelper.adjustReleaseTime(unadjustedFrameReleaseTimeNs);
              // earlyUs = (adjustedReleaseTimeNs - systemTimeNs) / 1000;
              
              if (Util.SDK_INT >= 21) {
                  // Let the underlying framework time the release.
                  if (earlyUs < 50000) {
                      try {
                          synchronized (lock) {
                              lock.wait();
                          }
                      } catch (InterruptedException e) {
                      }
                      //notifyFrameMetadataListener(presentationTimeUs, adjustedReleaseTimeNs, format);
                      renderOutputBufferV21(codec, bufferIndex, presentationTimeUs, adjustedReleaseTimeNs);
                      updateVideoFrameProcessingOffsetCounters(earlyUs);
                      return true;
                  }
              } else {
              }
              
              // We're either not playing, or it's not time to render the frame yet.
              return false;
          }
              
          private static boolean isBufferLate(long earlyUs) {
              // Class a buffer as late if it should have been presented more than 30 ms ago.
              return earlyUs < -30000;
          }
      }
      
      
      // exoplayer ver 2.18.1
      private static class CustomRenderersFactory2 extends DefaultRenderersFactory
      {
          public CustomRenderersFactory2(Context context) {
              super(context);
          }
          
          @Override
          public DefaultRenderersFactory setAllowedVideoJoiningTimeMs(long allowedVideoJoiningTimeMs) {
              super.setAllowedVideoJoiningTimeMs(allowedVideoJoiningTimeMs);
              return this;
          }
          
          @Override
          public Renderer[] createRenderers(
              Handler eventHandler,
              VideoRendererEventListener videoRendererEventListener,
              AudioRendererEventListener audioRendererEventListener,
              TextOutput textRendererOutput,
              MetadataOutput metadataRendererOutput) {
          
              Renderer[] renderers =
                  super.createRenderers(
                      eventHandler,
                      videoRendererEventListener,
                      audioRendererEventListener,
                      textRendererOutput,
                      metadataRendererOutput);
              
              ArrayList<Renderer> rendererList = new ArrayList<>(Arrays.asList(renderers));
              
              // The output latency of the engine can be used to compensate for sync
              double latency = engine.getOutputLatencyMs();
              
              // Audio: opus codec with the spatial audio engine
              // TBE_8_2 implies 10 channels of audio (8 channels of spatial audio, 2 channels of
              // head-locked)
              if (audio360Sink == null) {
                  audio360Sink = new Audio360Sink(spat, ChannelMap.TBE_8_2, latency);
              }
              final OpusRenderer audioRenderer = new OpusRenderer(audio360Sink);
              
              // place our audio renderer first in the list to prioritize it
              rendererList.add(0, audioRenderer);
              Log.e(TAG, "TEMP Added OpusRenderer to renderers list!!");
              
              renderers = rendererList.toArray(renderers);
              return renderers;
          }
          
          @Override
          protected void buildVideoRenderers(
              Context context,
              @ExtensionRendererMode int extensionRendererMode,
              MediaCodecSelector mediaCodecSelector,
              boolean enableDecoderFallback,
              Handler eventHandler,
              VideoRendererEventListener eventListener,
              long allowedVideoJoiningTimeMs,
              ArrayList<Renderer> out) {
          
              MediaCodecVideoRenderer videoRenderer =
                  new CustomMediaCodecVideoRenderer(
                      context,
                      getCodecAdapterFactory(),
                      mediaCodecSelector,
                      allowedVideoJoiningTimeMs,
                      enableDecoderFallback,
                      eventHandler,
                      eventListener,
                      MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
              out.add(videoRenderer);
              
              if (extensionRendererMode == EXTENSION_RENDERER_MODE_OFF) {
                  return;
              }
              int extensionRendererIndex = out.size();
              if (extensionRendererMode == EXTENSION_RENDERER_MODE_PREFER) {
                  extensionRendererIndex--;
              }
          
              try {
                  // Full class names used for constructor args so the LINT rule triggers if any of them move.
                  Class<?> clazz = Class.forName("androidx.media3.decoder.vp9.LibvpxVideoRenderer");
                  Constructor<?> constructor =
                      clazz.getConstructor(
                          long.class,
                          android.os.Handler.class,
                          com.google.android.exoplayer2.video.VideoRendererEventListener.class,
                          int.class);
                  Renderer renderer =
                      (Renderer)
                      constructor.newInstance(
                          allowedVideoJoiningTimeMs,
                          eventHandler,
                          eventListener,
                          MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
                  out.add(extensionRendererIndex++, renderer);
                  Log.i(TAG, "Loaded LibvpxVideoRenderer.");
              } catch (ClassNotFoundException e) {
                  // Expected if the app was built without the extension.
              } catch (Exception e) {
                  // The extension is present, but instantiation failed.
                  throw new RuntimeException("Error instantiating VP9 extension", e);
              }
          
              try {
                  // Full class names used for constructor args so the LINT rule triggers if any of them move.
                  Class<?> clazz = Class.forName("androidx.media3.decoder.av1.Libgav1VideoRenderer");
                  Constructor<?> constructor =
                      clazz.getConstructor(
                          long.class,
                          android.os.Handler.class,
                          com.google.android.exoplayer2.video.VideoRendererEventListener.class,
                          int.class);
                  Renderer renderer =
                      (Renderer)
                      constructor.newInstance(
                          allowedVideoJoiningTimeMs,
                          eventHandler,
                          eventListener,
                          MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY);
                  out.add(extensionRendererIndex++, renderer);
                  Log.i(TAG, "Loaded Libgav1VideoRenderer.");
              } catch (ClassNotFoundException e) {
                  // Expected if the app was built without the extension.
              } catch (Exception e) {
                  // The extension is present, but instantiation failed.
                  throw new RuntimeException("Error instantiating AV1 extension", e);
              }
          }
      }
      
      public static void onRenderEvent()
      

      Unityのメインスクリプトから呼ばれる関数です。
      スレッドのロックを全て解除します。

      protected boolean processOutputBuffer()
      

      ExoPlayerの出力処理の関数です これを自作のものに置き換えてスレッドをロックする仕組みを追加します。

      この関数は引数で渡されたベースタイム(positionUs)とフレームデータのプレゼンタイム(bufferPresentationTimeUs)を比較して

      1. まだ早いと判断した場合は何もせずfalseを返す。ほかの処理(オーディオストリームなど)をした後、再びprocessOutputBuffer()が呼ばれる
      2. 丁度いい時間と判断した場合はrenderOutputBufferV21()を実行してtrueを返す
      3. 大幅に遅れた場合はskipOutputBuffer()を実行してtrueを返す(ドロップフレーム、これを表示していると音ずれする)

      ザックリ言ってこのような働きをします。
      なので、丁度いいと判断した際にすぐに処理せずスレッドを止めて、onRenderEvent()から解除してもらうのを待つようにします。

      private static class CustomMediaCodecVideoRenderer extends MediaCodecVideoRenderer
      

      processOutputBuffer()はMediaCodecVideoRendererクラスのメンバーなので
      MediaCodecVideoRendererを継承した自作クラスCustomMediaCodecVideoRendererを作りオーバーライトします。

      private static class CustomRenderersFactory2 extends DefaultRenderersFactory
      

      自作したCustomMediaCodecVideoRendererを読み込むために用意したクラスです。
      CustomRenderersFactoryとしたいところですが、サンプルプログラムに同じ名前のクラスがあるので2を付けて区別しています。
      protected void buildVideoRenderers()をオーバーライトして自作したCustomMediaCodecVideoRendererを読み込みます。

    • 編集3
      自作したCustomRenderersFactory2をExoPlayerへ組み込む
      358行目あたりにあるExoPlayerのインスタンスを作るところでCustomRenderersFactoryを登録しているので コメントアウトして CustomRenderersFactory2へ差し替えます。

      NativeVideoPlayer.java 358行目あたり
          ExoPlayer.Builder playerBuilder =
              new ExoPlayer.Builder(context)
              .setMediaSourceFactory(buildMediaSourceFactory(context, dataSourceFactory))
              // .setRenderersFactory(new CustomRenderersFactory(context));
              .setRenderersFactory(new CustomRenderersFactory2(context));
      

    以上で完了です。
    ビルドして実行すればUnityのプレイヤー機能と同じようにExoPlayerでも なめらかな再生になります。

さいごに

今回の対策はリフレッシュレートと動画のフレームレートが近い場合専用です。

  • 60Hzのデイスプレイへ60fpsの動画を再生する場合のみうまく動作します
  • Quest3の場合は60Hzに出来ないので 72Hzで72fpsの動画を再生した場合のみ上手く動作します

リフレッシュレートより動画のfpsが高い場合は同期をとらない方が良いし、なによりprocessOutputBuffer()の多くの処理を省略しています。
本格的なメディアプレイヤーを制作する場合は さらに120Hzで60fpsだったり 72Hzで60fpsなどでも均等なタイミングで動作するような仕組みが必要となります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0