Highly optimized video decoding

still some problems with looping videos
This commit is contained in:
Flamefire committed 2014-02-27 20:28:16 +01:00
1 parent 82b650416f
commit 5245998662
3 files changed
+415 -286

No files matched your search

+79 -78
View File
@@ -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;
}
}
+286 -184
View File
@@ -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;
}
}
}
+50 -24
View File
@@ -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;
}
}
}