diff --git a/Vocaluxe/Lib/Video/Acinerella/CDecoder.cs b/Vocaluxe/Lib/Video/Acinerella/CDecoder.cs index a60359eb..db94ad2c 100644 --- a/Vocaluxe/Lib/Video/Acinerella/CDecoder.cs +++ b/Vocaluxe/Lib/Video/Acinerella/CDecoder.cs @@ -18,9 +18,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Runtime.InteropServices; -using System.Threading; -using Vocaluxe.Base; using VocaluxeLib.Draw; namespace Vocaluxe.Lib.Video.Acinerella @@ -28,46 +25,54 @@ namespace Vocaluxe.Lib.Video.Acinerella class CDecoder { private readonly Stopwatch _LoopTimer = new Stopwatch(); - private readonly Object _MutexSyncSignals = new Object(); - private float _LastShownTime; // time if the last shown frame in s - - private float _Time; + private float _LastShownTime = -1f; // time if the last shown frame in s private float _Gap; - private float _Start; - private bool _Loop; - - private float _SetTime; - private float _SetGap; - private float _SetStart; - private bool _SetSkip; - - private float _SkipTime; // = Start + VideoGap - - private string _FileName; // current video file name private bool _Paused; + private float _LoopTime; private CDecoderThread _Thread; - - public CDecoder() {} - public float Length { get; private set; } - - public bool Loop { private get; set; } - public bool Finished { get; private set; } + public CDecoder() + { + Length = 0; + Finished = true; + } + + public bool Loop + { + get { return _Thread.Loop; } + set + { + { + _Thread.Loop = value; + if (value) + { + _LoopTime = _Thread._RequestTime; + _LoopTimer.Restart(); + } + } + } + } + public bool Paused { set { - lock (_MutexSyncSignals) + if (_Paused == value) + return; + _Paused = value; + if (_Paused) { - _Paused = value; - if (_Paused) - _LoopTimer.Stop(); - else - _LoopTimer.Start(); + _LoopTimer.Stop(); + _Thread.Pause(); + } + else + { + _LoopTimer.Start(); + _Thread.Resume(); } } } @@ -80,35 +85,15 @@ namespace Vocaluxe.Lib.Video.Acinerella if (!File.Exists(fileName)) return false; - _FileName = fileName; - - //Do this here as one may want to get the length afterwards! - IntPtr pInstance = _LoadFile(); - if (pInstance == IntPtr.Zero) - return false; - - - _Thread = new CDecoderThread(Path.GetFileName(fileName), pInstance); - return true; - } - - //Open the file and get the length. - private IntPtr _LoadFile() - { - try + _Thread = new CDecoderThread(); + if (_Thread.LoadFile(fileName)) { - IntPtr pInstance = CAcinerella.AcInit(); - CAcinerella.AcOpen2(pInstance, _FileName); - - var instance = (SACInstance)Marshal.PtrToStructure(pInstance, typeof(SACInstance)); - Length = instance.Info.Duration / 1000f; - bool ok = instance.Opened; - if (ok) - return pInstance; + Length = _Thread.Length; + Finished = false; + return _Thread.Start(); } - catch (Exception) {} - CLog.LogError("Error opening video file: " + _FileName); - return IntPtr.Zero; + _Thread = null; + return false; } public void Close() @@ -116,45 +101,61 @@ namespace Vocaluxe.Lib.Video.Acinerella if (_Thread != null) _Thread.Stop(); Length = 0; + Finished = true; } public bool GetFrame(ref CTexture frame, float time, out float videoTime) { + if (Finished) + { + videoTime = Length - _Gap; + return false; + } if (Loop) { - lock (_MutexSyncSignals) + time = _LoopTime + _LoopTimer.ElapsedMilliseconds / 1000f; + if (time >= Length) { - _SetTime += _LoopTimer.ElapsedMilliseconds / 1000f; + do + { + time -= Length; + } while (time >= Length); + _LoopTime = time; _LoopTimer.Restart(); } } - else if (Math.Abs(_SetTime - time) >= 1f / 1000f) //Check 1 ms difference + else + time += _Gap; + _Thread.SyncTime(time); + + if (Math.Abs(_LastShownTime - time) < 1f / 1000 && frame != null) //Check 1 ms difference { - lock (_MutexSyncSignals) - { - _SetTime = time; - } - } - else if (Math.Abs(_LastShownTime - time) < 1f / 1000 && frame != null) //Check 1 ms difference - { - videoTime = _LastShownTime; + videoTime = _LastShownTime - _Gap; return true; } - _UploadNewFrame(ref frame); - videoTime = _LastShownTime; + CDecoderThread.EFrameState state = _Thread.GetFrame(ref frame, ref time); + switch (state) + { + case CDecoderThread.EFrameState.EndFrame: + Finished = true; + videoTime = Length - _Gap; + break; + case CDecoderThread.EFrameState.ValidFrame: + _LastShownTime = time; + videoTime = time - _Gap; + break; + default: + videoTime = _LastShownTime - _Gap; + break; + } return frame != null; } public bool Skip(float start, float gap) { - lock (_MutexSyncSignals) - { - _SetStart = start; - _SetGap = gap; - _SetSkip = true; - _NoMoreFrames = false; - Finished = false; - } + Finished = false; + _Gap = gap; + _Thread.Skip(start + gap); return true; } } diff --git a/Vocaluxe/Lib/Video/Acinerella/CDecoderThread.cs b/Vocaluxe/Lib/Video/Acinerella/CDecoderThread.cs index 7e40b96f..2f25a86a 100644 --- a/Vocaluxe/Lib/Video/Acinerella/CDecoderThread.cs +++ b/Vocaluxe/Lib/Video/Acinerella/CDecoderThread.cs @@ -1,51 +1,244 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.IO; using System.Runtime.InteropServices; -using System.Text; using System.Threading; using Vocaluxe.Base; using VocaluxeLib.Draw; namespace Vocaluxe.Lib.Video.Acinerella { + //This class describes a thread decoding a video + //All public methods are meant to be called from "reader" thread only + //Most others are to be called by this thread (_Thread instance) only! class CDecoderThread { - private readonly Thread _Thread; - private readonly Object _MutexFramebuffer = new Object(); - private bool isStarted; + public enum EFrameState + { + ValidFrame, + InvalidFrame, + EndFrame + } - private IntPtr _Instance; // acinerella instance + private Thread _Thread; + + private IntPtr _Instance = IntPtr.Zero; // acinerella instance private IntPtr _Videodecoder = IntPtr.Zero; // acinerella video decoder instance private readonly CFramebuffer _Framebuffer; private float _LastDecodedTime; // time of last decoded frame + // time of last requested frame aka current should-be position (_LastDecodedTime should be >=_RequestTime) + //HAS to be < Length + public float _RequestTime { get; private set; } + private bool _Paused; + private String _FileName; private float _FrameDuration; // frame time in s private int _Width; private int _Height; + private bool _RequestSkip; private bool _Terminated; private bool _FrameAvailable; private bool _NoMoreFrames; private readonly AutoResetEvent _EvWakeUp = new AutoResetEvent(false); + private readonly AutoResetEvent _EvPause = new AutoResetEvent(false); + private readonly AutoResetEvent _EvNoMoreFrames = new AutoResetEvent(false); private bool _IsSleeping; private int _WaitCount; - public CDecoderThread(String name, IntPtr instance) + public float Length { get; private set; } + public bool Loop { get; set; } + + public CDecoderThread() { _Framebuffer = new CFramebuffer(10); - _Instance = instance; - _Thread = new Thread(_Execute) {Priority = ThreadPriority.Normal, Name = name}; + } + + //Open the file and get the length. + public bool LoadFile(String fileName) + { + _FileName = fileName; + try + { + _Instance = CAcinerella.AcInit(); + CAcinerella.AcOpen2(_Instance, fileName); + + var instance = (SACInstance)Marshal.PtrToStructure(_Instance, typeof(SACInstance)); + Length = instance.Info.Duration / 1000f; + bool ok = instance.Opened && Length > 0.001f; + if (ok) + return true; + _Free(); + } + catch (Exception) {} + CLog.LogError("Error opening video file: " + _FileName); + _Instance = IntPtr.Zero; + return false; + } + + public bool Start() + { + if (_Instance == IntPtr.Zero) + { + CLog.LogError("Tried to start a video file that is not open: " + _FileName); + return false; + } + if (_Thread != null) + { + CLog.LogError("Tried to start a video file that is already started: " + _FileName); + return false; + } + _RequestTime = 0f; + _Thread = new Thread(_Execute) {Priority = ThreadPriority.Normal, Name = Path.GetFileName(_FileName)}; _Thread.Start(); + return true; } public void Stop() { _Terminated = true; + _EvPause.Set(); + _EvWakeUp.Set(); + _EvNoMoreFrames.Set(); } - private bool _DoOpen() + public void Pause() + { + if (_Paused) + return; + _EvPause.Reset(); + _Paused = true; + } + + public void Resume() + { + if (!_Paused) + return; + _Paused = false; + _EvPause.Set(); + } + + public void Skip(float time) + { + //TODO: Problem: This is not threadsave + //Scenario: Clear buffer from main thread, decoder is in _Decode or _Copy + //Result: Cleared buffer contains 1 old item. This is a problem when skipping back: E.g. Loop: + //Old item has time=Length->Will not get removed + //Workaround: Check in _FindFrame to remove first old item + _Framebuffer.Clear(); + + + _RequestTime = time; + _RequestSkip = true; + _EvNoMoreFrames.Set(); + } + + private CFramebuffer.CFrame _FindFrame(float now) + { + CFramebuffer.CFrame result = null; + float maxEnd = now - _FrameDuration * 2; //Get only frames younger than 2 + CFramebuffer.CFrame frame; + bool first = true; + _Framebuffer.ResetStack(); + while ((frame = _Framebuffer.Pop()) != null) + { + float frameEnd = frame.Time + _FrameDuration; + //Don't show frames that are shown during or after now + if (frameEnd >= now) + { + //All following frames are after that one -> BREAK + if (!first) + break; + //Well... Should be. Workaround: + CFramebuffer.CFrame nextFrame = _Framebuffer.Pop(); + if (nextFrame == null || nextFrame.Time > frame.Time) + break; + frame.SetRead(); //Discard wrong frame + frame = nextFrame; + //Replicate calculations + frameEnd = frame.Time + _FrameDuration; + if (frameEnd >= now) + break; + } + first = false; + //Get the last(newest) possible frame and skip the rest to force in-order showing + if (frameEnd <= maxEnd) + { + //Frame is to old -> Discard + frame.SetRead(); + } + else + { + maxEnd = frameEnd; + result = frame; + } + } + return result; + } + + //Time should be < Length + public void SyncTime(float time) + { + if (_Thread == null) + return; //Not initialized + while (time >= Length) + time -= Length; + if (_RequestTime - time >= 1f / 1000f) + { + //Jump back more than 1ms + //Will mostly happen in loops when looped through + Skip(time); + } + else if (time - _RequestTime >= 1f / 1000f) + { + //we were more than 1 ms to slow -> Jump forward + // ReSharper disable CompareOfFloatsByEqualityOperator + if (Loop && _RequestTime == 0f) + // ReSharper restore CompareOfFloatsByEqualityOperator + { + //Check if we had a loop + _Framebuffer.ResetStack(); + CFramebuffer.CFrame frame; + while ((frame = _Framebuffer.Pop()) != null) + { + if (frame.Time + _FrameDuration >= time) + return; + } + } + + _RequestTime = time; + } + } + + public EFrameState GetFrame(ref CTexture frame, ref float time) + { + EFrameState result; + CFramebuffer.CFrame curFrame = _FindFrame(time); + + if (curFrame != null) + { + CDraw.UpdateOrAddTexture(ref frame, _Width, _Height, curFrame.Data); + if (!_Paused) + curFrame.SetRead(); + time = curFrame.Time; + result = (frame != null) ? EFrameState.ValidFrame : EFrameState.InvalidFrame; + } + else + { + if (_NoMoreFrames && _Framebuffer.IsEmpty()) + result = EFrameState.EndFrame; + else + result = EFrameState.InvalidFrame; + } + if (_IsSleeping) + { + _IsSleeping = false; + _EvWakeUp.Set(); + } + return result; + } + + private bool _OpenVideoStream() { int videoStreamIndex; SACDecoder videodecoder; @@ -57,16 +250,12 @@ namespace Vocaluxe.Lib.Video.Acinerella } catch (Exception) { - CLog.LogError("Error opening video file (can't find decoder): " + _Thread.Name); - _Free(); + CLog.LogError("Error opening video file (can't find decoder): " + _FileName); return false; } if (videoStreamIndex < 0) - { - _Free(); return false; - } _Width = videodecoder.StreamInfo.VideoInfo.FrameWidth; _Height = videodecoder.StreamInfo.VideoInfo.FrameHeight; @@ -74,14 +263,12 @@ namespace Vocaluxe.Lib.Video.Acinerella if (videodecoder.StreamInfo.VideoInfo.FramesPerSecond > 0) _FrameDuration = 1f / (float)videodecoder.StreamInfo.VideoInfo.FramesPerSecond; - _LastDecodedTime = 0f; - _SetTime = 0f; - _Framebuffer.Init(_Width * _Height * 4); _FrameAvailable = false; return true; } + //Just call this if thread is not alive private void _Free() { if (_Videodecoder != IntPtr.Zero) @@ -94,50 +281,108 @@ namespace Vocaluxe.Lib.Video.Acinerella } } + // Skip to a given time (in s) private void _Skip() { - _SkipTime = _Start + _Gap; - - if (_SkipTime < 0) - _SkipTime = 0; + float skipTime = _RequestTime; + if (skipTime < 0 || skipTime >= Length) + skipTime = 0; try { - CAcinerella.AcSeek(_Videodecoder, -1, (Int64)(_SkipTime * 1000f)); + CAcinerella.AcSeek(_Videodecoder, -1, (Int64)(skipTime * 1000f)); } catch (Exception e) { CLog.LogError("Error seeking video file \"" + _FileName + "\": " + e.Message); } - _LastDecodedTime = _SkipTime; + _LastDecodedTime = skipTime; - lock (_MutexFramebuffer) + _FrameAvailable = false; + } + + private void _Decode() + { + const int minFrameDropCount = 4; + + if (_Paused || _NoMoreFrames) + return; + + float videoTime = _RequestTime; + float timeDifference = videoTime - _LastDecodedTime; + + bool dropFrame = timeDifference >= (minFrameDropCount - 1) * _FrameDuration; + + bool hasFrameDecoded = false; + if (dropFrame) { - _Framebuffer.Clear(); + try + { + var frameDropCount = (int)(timeDifference / _FrameDuration); + hasFrameDecoded = CAcinerella.AcSkipFrames(_Instance, _Videodecoder, frameDropCount); + } + catch (Exception) + { + CLog.LogError("Error AcSkipFrame " + _FileName); + } } + if (!hasFrameDecoded) + { + try + { + hasFrameDecoded = CAcinerella.AcGetFrame(_Instance, _Videodecoder); + } + catch (Exception) + { + CLog.LogError("Error AcGetFrame " + _FileName); + } + } + if (hasFrameDecoded) + _FrameAvailable = true; + else + { + if (Loop) + { + _RequestTime = 0; + _Skip(); + } + else + _NoMoreFrames = true; + } + } + + private void _CopyDecodedFrameToBuffer() + { + var videodecoder = (SACDecoder)Marshal.PtrToStructure(_Videodecoder, typeof(SACDecoder)); + if (videodecoder.Buffer != IntPtr.Zero) + { + _LastDecodedTime = (float)videodecoder.Timecode; + _Framebuffer.Put(videodecoder.Buffer, _LastDecodedTime); + } _FrameAvailable = false; } private void _Execute() { - if (!_DoOpen()) + if (!_OpenVideoStream()) + { + _Free(); Stop(); + } + while (!_Terminated) { - bool skip; - lock (_MutexSyncSignals) + if (_Paused) + _EvPause.WaitOne(); + if (_NoMoreFrames) + _EvNoMoreFrames.WaitOne(); + if (_RequestSkip) { - _Time = _SetTime; - skip = _SetSkip; - _SetSkip = false; - _Gap = _SetGap; - _Start = _SetStart; - _Loop = Loop; - } - - if (skip) + _RequestSkip = false; + _NoMoreFrames = false; _Skip(); + } if (!_FrameAvailable) _Decode(); @@ -146,9 +391,9 @@ namespace Vocaluxe.Lib.Video.Acinerella { if (!_Framebuffer.IsFull()) { - _Copy(); + _CopyDecodedFrameToBuffer(); _WaitCount = 0; - Thread.Sleep(1); + Thread.Sleep(5); } else if (_WaitCount > 3) { @@ -165,148 +410,5 @@ namespace Vocaluxe.Lib.Video.Acinerella _Free(); } - - private void _Decode() - { - const int minFrameDropCount = 4; - - if (_Paused || _NoMoreFrames) - return; - - float videoTime = _Time + _Gap; - float timeDifference = videoTime - _LastDecodedTime; - - bool dropFrame = timeDifference >= (minFrameDropCount - 1) * _FrameDuration; - - if (_Terminated) - return; - - bool frameFinished = false; - if (dropFrame) - { - try - { - if (videoTime >= Length) - { - if (_Loop) - { - lock (_MutexSyncSignals) - { - _Start = 0; - _Gap = 0f; - _SetTime = 0; - } - _Skip(); - } - else - { - _NoMoreFrames = true; - return; - } - } - else - { - var frameDropCount = (int)(timeDifference / _FrameDuration); - frameFinished = CAcinerella.AcSkipFrames(_Instance, _Videodecoder, frameDropCount); - } - } - catch (Exception) - { - CLog.LogError("Error AcSkipFrame " + _Thread.Name); - } - } - - if (!frameFinished) - { - try - { - frameFinished = CAcinerella.AcGetFrame(_Instance, _Videodecoder); - } - catch (Exception) - { - CLog.LogError("Error AcGetFrame " + _Thread.Name); - } - } - if (frameFinished) - _FrameAvailable = true; - else - { - if (_Loop) - { - lock (_MutexSyncSignals) - { - _Start = 0f; - _Gap = 0f; - _SetTime = 0f; - } - _Skip(); - } - else - _NoMoreFrames = true; - } - } - - private void _Copy() - { - var videodecoder = (SACDecoder)Marshal.PtrToStructure(_Videodecoder, typeof(SACDecoder)); - if (videodecoder.Buffer == IntPtr.Zero) - return; - _LastDecodedTime = (float)videodecoder.Timecode; - _Framebuffer.Put(videodecoder.Buffer, _LastDecodedTime); - _FrameAvailable = false; - } - - private void _UploadNewFrame(ref CTexture frame) - { - lock (_MutexFramebuffer) - { - CFrame curFrame = _FindFrame(); - - if (curFrame != null) - { - CDraw.UpdateOrAddTexture(ref frame, _Width, _Height, curFrame.Data); - - lock (_MutexSyncSignals) - { - _LastShownTime = curFrame.Time; - } - } - else - { - if (_NoMoreFrames && _Framebuffer.IsEmpty()) - Finished = true; - } - } - if (!Finished && _IsSleeping) - { - _IsSleeping = false; - _EvWakeUp.Set(); - } - } - - private CFrame _FindFrame() - { - CFrame result = null; - float now = _SetTime + _Gap; - float maxEnd = now - _FrameDuration * 2; //Get only frames younger than 2 - CFrame frame; - bool paused = _Paused; - while ((frame = _Framebuffer.Get()) != null) - { - float frameEnd = frame.Time + _FrameDuration; - //Don't show frames that are shown during or after now - if (frameEnd >= now) - break; //All following frames are after that one - if (paused) // Don't modify anything - break; - _Framebuffer.SetRead(); - //Get the last(newest) possible frame and skip the rest to force in-order showing - if (frameEnd <= maxEnd) - continue; - maxEnd = frameEnd; - result = frame; - } - return result; - } } } \ No newline at end of file diff --git a/Vocaluxe/Lib/Video/Acinerella/CFramebuffer.cs b/Vocaluxe/Lib/Video/Acinerella/CFramebuffer.cs index b0c2704f..fe9a42a5 100644 --- a/Vocaluxe/Lib/Video/Acinerella/CFramebuffer.cs +++ b/Vocaluxe/Lib/Video/Acinerella/CFramebuffer.cs @@ -20,24 +20,35 @@ using System.Runtime.InteropServices; namespace Vocaluxe.Lib.Video.Acinerella { - class CFrame - { - public readonly byte[] Data; - public float Time = -1; - - public CFrame(int dataSize) - { - Data = new byte[dataSize]; - } - } - class CFramebuffer { + public class CFrame + { + private readonly CFramebuffer _Parent; + private readonly int _Index; + public readonly byte[] Data; + public float Time = -1; + + public CFrame(CFramebuffer parent, int index, int dataSize) + { + _Parent = parent; + _Index = index; + Data = new byte[dataSize]; + } + + //Only call from reader thread + public void SetRead() + { + _Parent._SetRead(_Index); + } + } + private readonly CFrame[] _Frames; private readonly int _Size; private int _DataSize; private int _Last; private int _First; + private int _Next; private bool _Initialized; public int Size @@ -45,21 +56,24 @@ namespace Vocaluxe.Lib.Video.Acinerella get { return _Size; } } + // Constructs a framebuffer with max. size frames public CFramebuffer(int size) { _Size = size; _Frames = new CFrame[size]; } + // Initializes the framebuffer with the size of each frame + // MUST be called before all others public void Init(int dataSize) { _DataSize = dataSize; for (int i = 0; i < _Size; i++) - _Frames[i] = new CFrame(dataSize); + _Frames[i] = new CFrame(this, i, dataSize); _Initialized = true; } - private int _Next(int current) + private int _GetNextIndex(int current) { current++; return (current < _Size) ? current : 0; @@ -67,7 +81,7 @@ namespace Vocaluxe.Lib.Video.Acinerella public bool IsFull() { - return _Next(_Last) == _First; + return _GetNextIndex(_Last) == _First; } public bool IsEmpty() @@ -75,6 +89,7 @@ namespace Vocaluxe.Lib.Video.Acinerella return _First == _Last; } + //Only call from writer thread public bool Put(IntPtr data, float time) { if (!_Initialized || IsFull()) @@ -82,27 +97,38 @@ namespace Vocaluxe.Lib.Video.Acinerella CFrame frame = _Frames[_Last]; Marshal.Copy(data, frame.Data, 0, _DataSize); frame.Time = time; - _Last = _Next(_Last); + _Last = _GetNextIndex(_Last); return true; } - public CFrame Get() + //Only call from reader thread + public void ResetStack() { - return IsEmpty() ? null : _Frames[_First]; + _Next = _First; } - public bool SetRead() + //Pops the next frame if available + //You have to call ResetNext before the first call to this function and call the CFrame.SetRead to free the buffers + //Only call from reader thread + public CFrame Pop() { - if (IsEmpty()) - return false; - _First = _Next(_First); - return true; + if (_Next == _Last) + return null; + CFrame res = _Frames[_Next]; + _Next = _GetNextIndex(_Next); + return res; } + private void _SetRead(int index) + { + if ((_First < index && (index < _Next || _Next < _First)) || (_Next < _First && index < _Next)) + _First = _GetNextIndex(index); + } + + //Only call from reader thread public void Clear() { - _First = 0; - _Last = 0; + _Next = _First = _Last; } } } \ No newline at end of file