123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594 |
- using System.Collections;
- using System.Collections.Generic;
- using UnityEngine;
- //-----------------------------------------------------------------------------
- // Copyright 2015-2021 RenderHeads Ltd. All rights reserved.
- //-----------------------------------------------------------------------------
- namespace RenderHeads.Media.AVProVideo
- {
- /// <summary>
- /// Utility class to resample MediaPlayer video frames to allow for smoother playback
- /// Keeps a buffer of frames with timestamps and presents them using its own clock
- /// </summary>
- public class Resampler
- {
- private class TimestampedRenderTexture
- {
- public RenderTexture texture = null;
- public long timestamp = 0;
- public bool used = false;
- }
- public enum ResampleMode
- {
- POINT, LINEAR
- }
- private List<TimestampedRenderTexture[]> _buffer = new List<TimestampedRenderTexture[]>();
- private MediaPlayer _mediaPlayer;
- private RenderTexture[] _outputTexture = null;
- private int _start = 0;
- private int _end = 0;
- private int _bufferSize = 0;
- private long _baseTimestamp = 0;
- private float _elapsedTimeSinceBase = 0f;
- private Material _blendMat;
- private ResampleMode _resampleMode;
- private string _name = "";
- private long _lastTimeStamp = -1;
- private int _droppedFrames = 0;
- private long _lastDisplayedTimestamp = 0;
- private int _frameDisplayedTimer = 0;
- private long _currentDisplayedTimestamp = 0;
- public int DroppedFrames
- {
- get { return _droppedFrames; }
- }
- public int FrameDisplayedTimer
- {
- get { return _frameDisplayedTimer; }
- }
- public long BaseTimestamp
- {
- get { return _baseTimestamp; }
- set { _baseTimestamp = value; }
- }
- public float ElapsedTimeSinceBase
- {
- get { return _elapsedTimeSinceBase; }
- set { _elapsedTimeSinceBase = value; }
- }
- public float LastT
- {
- get; private set;
- }
- public long TextureTimeStamp
- {
- get; private set;
- }
- private const string ShaderPropT = "_t";
- private const string ShaderPropAftertex = "_AfterTex";
- private int _propAfterTex;
- private int _propT;
- private float _videoFrameRate;
- public void OnVideoEvent(MediaPlayer mp, MediaPlayerEvent.EventType et, ErrorCode errorCode)
- {
- switch (et)
- {
- case MediaPlayerEvent.EventType.MetaDataReady:
- _videoFrameRate = mp.Info.GetVideoFrameRate();
- _elapsedTimeSinceBase = 0f;
- if (_videoFrameRate > 0f)
- {
- _elapsedTimeSinceBase = _bufferSize / _videoFrameRate;
- }
- break;
- case MediaPlayerEvent.EventType.Closing:
- Reset();
- break;
- default:
- break;
- }
- }
- public Resampler(MediaPlayer player, string name, int bufferSize = 2, ResampleMode resampleMode = ResampleMode.LINEAR)
- {
- _bufferSize = Mathf.Max(2, bufferSize);
- player.Events.AddListener(OnVideoEvent);
- _mediaPlayer = player;
- Shader blendShader = Shader.Find("AVProVideo/Internal/BlendFrames");
- if (blendShader != null)
- {
- _blendMat = new Material(blendShader);
- _propT = Shader.PropertyToID(ShaderPropT);
- _propAfterTex = Shader.PropertyToID(ShaderPropAftertex);
- }
- else
- {
- Debug.LogError("[AVProVideo] Failed to find BlendFrames shader");
- }
- _resampleMode = resampleMode;
- _name = name;
- Debug.Log("[AVProVideo] Resampler " + _name + " started");
- }
- public Texture[] OutputTexture
- {
- get { return _outputTexture; }
- }
- public void Reset()
- {
- _lastTimeStamp = -1;
- _baseTimestamp = 0;
- InvalidateBuffer();
- }
- public void Release()
- {
- ReleaseRenderTextures();
- if (_blendMat != null)
- {
- if (Application.isPlaying)
- {
- Material.Destroy(_blendMat);
- }
- else
- {
- Material.DestroyImmediate(_blendMat);
- }
- }
- }
- private void ReleaseRenderTextures()
- {
- for (int i = 0; i < _buffer.Count; ++i)
- {
- for (int j = 0; j < _buffer[i].Length; ++j)
- {
- if (_buffer[i][j].texture != null)
- {
- RenderTexture.ReleaseTemporary(_buffer[i][j].texture);
- _buffer[i][j].texture = null;
- }
- }
- if (_outputTexture != null && _outputTexture[i] != null)
- {
- RenderTexture.ReleaseTemporary(_outputTexture[i]);
- }
- }
- _outputTexture = null;
- }
- private void ConstructRenderTextures()
- {
- ReleaseRenderTextures();
- _buffer.Clear();
- _outputTexture = new RenderTexture[_mediaPlayer.TextureProducer.GetTextureCount()];
- for (int i = 0; i < _mediaPlayer.TextureProducer.GetTextureCount(); ++i)
- {
- Texture tex = _mediaPlayer.TextureProducer.GetTexture(i);
- _buffer.Add(new TimestampedRenderTexture[_bufferSize]);
- for (int j = 0; j < _bufferSize; ++j)
- {
- _buffer[i][j] = new TimestampedRenderTexture();
- }
- for (int j = 0; j < _buffer[i].Length; ++j)
- {
- _buffer[i][j].texture = RenderTexture.GetTemporary(tex.width, tex.height, 0);
- _buffer[i][j].timestamp = 0;
- _buffer[i][j].used = false;
- }
- _outputTexture[i] = RenderTexture.GetTemporary(tex.width, tex.height, 0);
- _outputTexture[i].filterMode = tex.filterMode;
- _outputTexture[i].wrapMode = tex.wrapMode;
- _outputTexture[i].anisoLevel = tex.anisoLevel;
- // TODO: set up the mips level too?
- }
- }
- private bool CheckRenderTexturesValid()
- {
- for (int i = 0; i < _mediaPlayer.TextureProducer.GetTextureCount(); ++i)
- {
- Texture tex = _mediaPlayer.TextureProducer.GetTexture(i);
- for (int j = 0; j < _buffer.Count; ++j)
- {
- if (_buffer[i][j].texture == null || _buffer[i][j].texture.width != tex.width || _buffer[i][j].texture.height != tex.height)
- {
- return false;
- }
- }
- if (_outputTexture == null || _outputTexture[i] == null || _outputTexture[i].width != tex.width || _outputTexture[i].height != tex.height)
- {
- return false;
- }
- }
- return true;
- }
- //finds closest frame that occurs before given index
- private int FindBeforeFrameIndex(int frameIdx)
- {
- if (frameIdx >= _buffer.Count)
- {
- return -1;
- }
- int foundFrame = -1;
- float smallestDif = float.MaxValue;
- int closest = -1;
- float smallestElapsed = float.MaxValue;
- for (int i = 0; i < _buffer[frameIdx].Length; ++i)
- {
- if (_buffer[frameIdx][i].used)
- {
- float elapsed = (_buffer[frameIdx][i].timestamp - _baseTimestamp) / 10000000f;
- //keep track of closest after frame, just in case no before frame was found
- if (elapsed < smallestElapsed)
- {
- closest = i;
- smallestElapsed = elapsed;
- }
- float dif = _elapsedTimeSinceBase - elapsed;
- if (dif >= 0 && dif < smallestDif)
- {
- smallestDif = dif;
- foundFrame = i;
- }
- }
- }
- if (foundFrame < 0)
- {
- if (closest < 0)
- {
- return -1;
- }
- return closest;
- }
- return foundFrame;
- }
- private int FindClosestFrame(int frameIdx)
- {
- if (frameIdx >= _buffer.Count)
- {
- return -1;
- }
- int foundPos = -1;
- float smallestDif = float.MaxValue;
- for (int i = 0; i < _buffer[frameIdx].Length; ++i)
- {
- if (_buffer[frameIdx][i].used)
- {
- float elapsed = (_buffer[frameIdx][i].timestamp - _baseTimestamp) / 10000000f;
- float dif = Mathf.Abs(_elapsedTimeSinceBase - elapsed);
- if (dif < smallestDif)
- {
- foundPos = i;
- smallestDif = dif;
- }
- }
- }
- return foundPos;
- }
- //point update selects closest frame and uses that as output
- private void PointUpdate()
- {
- for (int i = 0; i < _buffer.Count; ++i)
- {
- int frameIndex = FindClosestFrame(i);
- if (frameIndex < 0)
- {
- continue;
- }
- _outputTexture[i].DiscardContents();
- Graphics.Blit(_buffer[i][frameIndex].texture, _outputTexture[i]);
- TextureTimeStamp = _currentDisplayedTimestamp = _buffer[i][frameIndex].timestamp;
- }
- }
- //Updates currently displayed frame
- private void SampleFrame(int frameIdx, int bufferIdx)
- {
- _outputTexture[bufferIdx].DiscardContents();
- Graphics.Blit(_buffer[bufferIdx][frameIdx].texture, _outputTexture[bufferIdx]);
- TextureTimeStamp = _currentDisplayedTimestamp = _buffer[bufferIdx][frameIdx].timestamp;
- }
- //Same as sample frame, but does a lerp of the two given frames and outputs that image instead
- private void SampleFrames(int bufferIdx, int frameIdx1, int frameIdx2, float t)
- {
- _blendMat.SetFloat(_propT, t);
- _blendMat.SetTexture(_propAfterTex, _buffer[bufferIdx][frameIdx2].texture);
- _outputTexture[bufferIdx].DiscardContents();
- Graphics.Blit(_buffer[bufferIdx][frameIdx1].texture, _outputTexture[bufferIdx], _blendMat);
- TextureTimeStamp = (long)Mathf.Lerp(_buffer[bufferIdx][frameIdx1].timestamp, _buffer[bufferIdx][frameIdx2].timestamp, t);
- _currentDisplayedTimestamp = _buffer[bufferIdx][frameIdx1].timestamp;
- }
- private void LinearUpdate()
- {
- for (int i = 0; i < _buffer.Count; ++i)
- {
- //find closest frame
- int frameIndex = FindBeforeFrameIndex(i);
- //no valid frame, this should never ever happen actually...
- if (frameIndex < 0)
- {
- continue;
- }
- //resample or just use last frame and set current elapsed time to that frame
- float frameElapsed = (_buffer[i][frameIndex].timestamp - _baseTimestamp) / 10000000f;
- if (frameElapsed > _elapsedTimeSinceBase)
- {
- SampleFrame(frameIndex, i);
- LastT = -1f;
- }
- else
- {
- int next = (frameIndex + 1) % _buffer[i].Length;
- float nextElapsed = (_buffer[i][next].timestamp - _baseTimestamp) / 10000000f;
- //no larger frame, move elapsed time back a bit since we cant predict the future
- if (nextElapsed < frameElapsed)
- {
- SampleFrame(frameIndex, i);
- LastT = 2f;
- }
- //have a before and after frame, interpolate
- else
- {
- float range = nextElapsed - frameElapsed;
- float t = (_elapsedTimeSinceBase - frameElapsed) / range;
- SampleFrames(i, frameIndex, next, t);
- LastT = t;
- }
- }
- }
- }
- private void InvalidateBuffer()
- {
- _elapsedTimeSinceBase = (_bufferSize / 2) / _videoFrameRate;
- for (int i = 0; i < _buffer.Count; ++i)
- {
- for (int j = 0; j < _buffer[i].Length; ++j)
- {
- _buffer[i][j].used = false;
- }
- }
- _start = _end = 0;
- }
- private float GuessFrameRate()
- {
- int fpsCount = 0;
- long fps = 0;
-
- for (int k = 0; k < _buffer[0].Length; k++)
- {
- if (_buffer[0][k].used)
- {
- // Find the pair with the smallest difference
- long smallestDiff = long.MaxValue;
- for (int j = k + 1; j < _buffer[0].Length; j++)
- {
- if (_buffer[0][j].used)
- {
- long diff = System.Math.Abs(_buffer[0][k].timestamp - _buffer[0][j].timestamp);
- if (diff < smallestDiff)
- {
- smallestDiff = diff;
- }
- }
- }
- if (smallestDiff != long.MaxValue)
- {
- fps += smallestDiff;
- fpsCount++;
- }
- }
- }
- if (fpsCount > 1)
- {
- fps /= fpsCount;
- }
- return 10000000f / (float)fps;
- }
- public void Update()
- {
- if (_mediaPlayer.TextureProducer == null)
- {
- return;
- }
- //recreate textures if invalid
- if (_mediaPlayer.TextureProducer == null || _mediaPlayer.TextureProducer.GetTexture() == null)
- {
- return;
- }
- if (!CheckRenderTexturesValid())
- {
- ConstructRenderTextures();
- }
- long currentTimestamp = _mediaPlayer.TextureProducer.GetTextureTimeStamp();
- //if frame has been updated, do a calculation to estimate dropped frames
- if (currentTimestamp != _lastTimeStamp)
- {
- float dif = Mathf.Abs(currentTimestamp - _lastTimeStamp);
- float frameLength = (10000000f / _videoFrameRate);
- if (dif > frameLength * 1.1f && dif < frameLength * 3.1f)
- {
- _droppedFrames += (int)((dif - frameLength) / frameLength + 0.5);
- }
- _lastTimeStamp = currentTimestamp;
- }
- //Adding texture to buffer logic
- long timestamp = _mediaPlayer.TextureProducer.GetTextureTimeStamp();
- bool insertNewFrame = !_mediaPlayer.Control.IsSeeking();
- //if buffer is not empty, we need to check if we need to reject the new frame
- if (_start != _end || _buffer[0][_end].used)
- {
- int lastFrame = (_end + _buffer[0].Length - 1) % _buffer[0].Length;
- //frame is not new and thus we do not need to store it
- if (timestamp == _buffer[0][lastFrame].timestamp)
- {
- insertNewFrame = false;
- }
- }
- bool bufferWasNotFull = (_start != _end) || (!_buffer[0][_end].used);
- if (insertNewFrame)
- {
- //buffer empty, reset base timestamp to current
- if (_start == _end && !_buffer[0][_end].used)
- {
- _baseTimestamp = timestamp;
- }
- //update buffer counters, if buffer is full, we get rid of the earliest frame by incrementing the start counter
- if (_end == _start && _buffer[0][_end].used)
- {
- _start = (_start + 1) % _buffer[0].Length;
- }
- for (int i = 0; i < _mediaPlayer.TextureProducer.GetTextureCount(); ++i)
- {
- Texture currentTexture = _mediaPlayer.TextureProducer.GetTexture(i);
- //store frame info
- _buffer[i][_end].texture.DiscardContents();
- Graphics.Blit(currentTexture, _buffer[i][_end].texture);
- _buffer[i][_end].timestamp = timestamp;
- _buffer[i][_end].used = true;
- }
- _end = (_end + 1) % _buffer[0].Length;
- }
- bool bufferNotFull = (_start != _end) || (!_buffer[0][_end].used);
- if (bufferNotFull)
- {
- for (int i = 0; i < _buffer.Count; ++i)
- {
- _outputTexture[i].DiscardContents();
- Graphics.Blit(_buffer[i][_start].texture, _outputTexture[i]);
- _currentDisplayedTimestamp = _buffer[i][_start].timestamp;
- }
- }
- else
- {
- // If we don't have a valid frame rate and the buffer is now full, guess the frame rate by looking at the buffered timestamps
- if (bufferWasNotFull && _videoFrameRate <= 0f)
- {
- _videoFrameRate = GuessFrameRate();
- _elapsedTimeSinceBase = (_bufferSize / 2) / _videoFrameRate;
- }
- }
- if (_mediaPlayer.Control.IsPaused())
- {
- InvalidateBuffer();
- }
- //we always wait until buffer is full before display things, just assign first frame in buffer to output so that the user can see something
- if (bufferNotFull)
- {
- return;
- }
- if (_mediaPlayer.Control.IsPlaying() && !_mediaPlayer.Control.IsFinished())
- {
- //correct elapsed time if too far out
- long ts = _buffer[0][(_start + _bufferSize / 2) % _bufferSize].timestamp - _baseTimestamp;
- double dif = Mathf.Abs(((float)((double)_elapsedTimeSinceBase * 10000000) - ts));
- double threshold = (_buffer[0].Length / 2) / _videoFrameRate * 10000000;
- if (dif > threshold)
- {
- _elapsedTimeSinceBase = ts / 10000000f;
- }
- if (_resampleMode == ResampleMode.POINT)
- {
- PointUpdate();
- }
- else if (_resampleMode == ResampleMode.LINEAR)
- {
- LinearUpdate();
- }
- _elapsedTimeSinceBase += Time.unscaledDeltaTime;
- }
- }
- public void UpdateTimestamp()
- {
- if (_lastDisplayedTimestamp != _currentDisplayedTimestamp)
- {
- _lastDisplayedTimestamp = _currentDisplayedTimestamp;
- _frameDisplayedTimer = 0;
- }
- _frameDisplayedTimer++;
- }
- }
- }
|