Add audio playback support to video player
This commit is contained in:
@@ -162,22 +162,58 @@ namespace
|
|||||||
|
|
||||||
bool ReadStream(VideoBackendPlayer& player, VideoPlayerMF& playerMF, DWORD streamIndex, TimeSpan dt)
|
bool ReadStream(VideoBackendPlayer& player, VideoPlayerMF& playerMF, DWORD streamIndex, TimeSpan dt)
|
||||||
{
|
{
|
||||||
|
PROFILE_CPU_NAMED("ReadStream");
|
||||||
|
ZoneText(player.DebugUrl, player.DebugUrlLen);
|
||||||
const bool isVideo = streamIndex == MF_SOURCE_READER_FIRST_VIDEO_STREAM;
|
const bool isVideo = streamIndex == MF_SOURCE_READER_FIRST_VIDEO_STREAM;
|
||||||
const bool isAudio = streamIndex == MF_SOURCE_READER_FIRST_AUDIO_STREAM;
|
const bool isAudio = streamIndex == MF_SOURCE_READER_FIRST_AUDIO_STREAM;
|
||||||
const TimeSpan lastFrameTime = isVideo ? player.VideoFrameTime : player.AudioBufferTime;
|
int32 goodSamples = 1;
|
||||||
const TimeSpan lastFrameDuration = isVideo ? player.VideoFrameDuration : player.AudioBufferDuration;
|
TimeSpan validTimeRangeStart(0), validTimeRangeEnd(0);
|
||||||
|
if (isAudio)
|
||||||
// Check if the current frame is valid (eg. when playing 24fps video at 60fps)
|
|
||||||
if (lastFrameDuration.Ticks > 0 &&
|
|
||||||
Math::IsInRange(playerMF.Time, lastFrameTime, lastFrameTime + lastFrameDuration))
|
|
||||||
{
|
{
|
||||||
return false;
|
constexpr int32 AudioFramesQueue = 10; // How many frames to read into the audio buffers queue in advance (to improve audio playback smoothness)
|
||||||
|
if (player.AudioBufferDuration.Ticks == 0)
|
||||||
|
{
|
||||||
|
// Read more samples for audio to enqueue multiple audio buffers for smoother playback
|
||||||
|
goodSamples = AudioFramesQueue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Skip reading if the last sample was already over this range (we've got enough in a queue)
|
||||||
|
validTimeRangeStart = player.AudioBufferTime - player.AudioBufferDuration * AudioFramesQueue;
|
||||||
|
validTimeRangeEnd = validTimeRangeStart + player.AudioBufferDuration;
|
||||||
|
if (Math::IsInRange(playerMF.Time, validTimeRangeStart, validTimeRangeEnd))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow to read future samples within queue range
|
||||||
|
validTimeRangeStart = player.AudioBufferTime - player.AudioBufferDuration;
|
||||||
|
validTimeRangeEnd = player.AudioBufferTime + player.AudioBufferDuration * AudioFramesQueue;
|
||||||
|
|
||||||
|
// Read more samples to keep queue at capacity
|
||||||
|
TimeSpan targetQueueEnd = playerMF.Time + player.AudioBufferDuration * AudioFramesQueue;
|
||||||
|
TimeSpan activeBufferEnd = player.AudioBufferTime + player.AudioBufferDuration;
|
||||||
|
TimeSpan missingQueueDuration = targetQueueEnd - activeBufferEnd;
|
||||||
|
goodSamples = (int32)Math::DivideAndRoundUp(missingQueueDuration.Ticks, player.AudioBufferDuration.Ticks);
|
||||||
|
if (goodSamples < 1)
|
||||||
|
goodSamples = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (isVideo)
|
||||||
|
{
|
||||||
|
// Check if the current frame is valid (eg. when playing 24fps video at 60fps)
|
||||||
|
if (player.VideoFrameDuration.Ticks > 0 &&
|
||||||
|
Math::IsInRange(playerMF.Time, player.VideoFrameTime, player.VideoFrameTime + player.VideoFrameDuration))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read samples until frame is matching the current time
|
// Read samples until frame is matching the current time
|
||||||
int32 samplesLeft = 500;
|
int32 samplesLeft = 500;
|
||||||
|
int32 goodSamplesLeft = goodSamples;
|
||||||
HRESULT hr;
|
HRESULT hr;
|
||||||
for (; samplesLeft > 0; samplesLeft--)
|
for (; samplesLeft > 0 && goodSamplesLeft > 0; samplesLeft--)
|
||||||
{
|
{
|
||||||
// Read sample
|
// Read sample
|
||||||
DWORD flags = 0;
|
DWORD flags = 0;
|
||||||
@@ -199,7 +235,11 @@ namespace
|
|||||||
franeDuration.Ticks = sampleDuration;
|
franeDuration.Ticks = sampleDuration;
|
||||||
}
|
}
|
||||||
//const int32 framesToTime = (playerMF.Time.Ticks - frameTime.Ticks) / franeDuration.Ticks;
|
//const int32 framesToTime = (playerMF.Time.Ticks - frameTime.Ticks) / franeDuration.Ticks;
|
||||||
const bool isGoodSample = Math::IsInRange(playerMF.Time, frameTime, frameTime + franeDuration);
|
bool isGoodSample = goodSamples != goodSamplesLeft; // If we've reached good frame, then use following frames too
|
||||||
|
if (validTimeRangeStart.Ticks != 0)
|
||||||
|
isGoodSample |= Math::IsInRange(frameTime, validTimeRangeStart, validTimeRangeEnd); // Ensure frame hits the valid range
|
||||||
|
else
|
||||||
|
isGoodSample |= Math::IsInRange(playerMF.Time, frameTime, frameTime + franeDuration); // Ensure current time hits this frame range
|
||||||
|
|
||||||
// Process sample
|
// Process sample
|
||||||
if (sample && isGoodSample)
|
if (sample && isGoodSample)
|
||||||
@@ -288,6 +328,8 @@ namespace
|
|||||||
}
|
}
|
||||||
if (sample)
|
if (sample)
|
||||||
sample->Release();
|
sample->Release();
|
||||||
|
if (isGoodSample)
|
||||||
|
goodSamplesLeft--;
|
||||||
|
|
||||||
if (flags & MF_SOURCE_READERF_ENDOFSTREAM)
|
if (flags & MF_SOURCE_READERF_ENDOFSTREAM)
|
||||||
{
|
{
|
||||||
@@ -299,10 +341,6 @@ namespace
|
|||||||
// Format/metadata might have changed so update the stream
|
// Format/metadata might have changed so update the stream
|
||||||
Configure(player, playerMF, streamIndex);
|
Configure(player, playerMF, streamIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// End loop if got good sample or need to seek back
|
|
||||||
if (isGoodSample)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// True if run out of samples and failed to get frame for the current time
|
// True if run out of samples and failed to get frame for the current time
|
||||||
@@ -354,6 +392,7 @@ bool VideoBackendMF::Player_Create(const VideoBackendPlayerInfo& info, VideoBack
|
|||||||
player.Backend = this;
|
player.Backend = this;
|
||||||
playerMF.Loop = info.Loop;
|
playerMF.Loop = info.Loop;
|
||||||
playerMF.FirstFrame = 1;
|
playerMF.FirstFrame = 1;
|
||||||
|
player.Created(info);
|
||||||
Players.Add(&player);
|
Players.Add(&player);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -493,6 +532,9 @@ void VideoBackendMF::Base_Update()
|
|||||||
SEEK_START:
|
SEEK_START:
|
||||||
if (playerMF.Seek)
|
if (playerMF.Seek)
|
||||||
{
|
{
|
||||||
|
// Reset cached frames timings
|
||||||
|
player.VideoFrameDuration = player.AudioBufferDuration = TimeSpan::Zero();
|
||||||
|
|
||||||
seeks++;
|
seeks++;
|
||||||
playerMF.Seek = 0;
|
playerMF.Seek = 0;
|
||||||
PROPVARIANT var;
|
PROPVARIANT var;
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ struct VideoBackendPlayer
|
|||||||
VideoBackend* Backend;
|
VideoBackend* Backend;
|
||||||
GPUTexture* Frame;
|
GPUTexture* Frame;
|
||||||
GPUBuffer* FrameUpload;
|
GPUBuffer* FrameUpload;
|
||||||
|
class GPUUploadVideoFrameTask* UploadVideoFrameTask;
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
Char* DebugUrl;
|
||||||
|
int32 DebugUrlLen;
|
||||||
|
#endif
|
||||||
int32 Width, Height, AvgVideoBitRate, FramesCount;
|
int32 Width, Height, AvgVideoBitRate, FramesCount;
|
||||||
int32 VideoFrameWidth, VideoFrameHeight;
|
int32 VideoFrameWidth, VideoFrameHeight;
|
||||||
PixelFormat Format;
|
PixelFormat Format;
|
||||||
@@ -35,9 +40,9 @@ struct VideoBackendPlayer
|
|||||||
TimeSpan AudioBufferTime, AudioBufferDuration;
|
TimeSpan AudioBufferTime, AudioBufferDuration;
|
||||||
AudioDataInfo AudioInfo;
|
AudioDataInfo AudioInfo;
|
||||||
BytesContainer VideoFrameMemory;
|
BytesContainer VideoFrameMemory;
|
||||||
uint32 AudioBuffer;
|
|
||||||
uint32 AudioSource;
|
uint32 AudioSource;
|
||||||
class GPUUploadVideoFrameTask* UploadVideoFrameTask;
|
uint32 NextAudioBuffer;
|
||||||
|
uint32 AudioBuffers[30];
|
||||||
uintptr BackendState[8];
|
uintptr BackendState[8];
|
||||||
|
|
||||||
VideoBackendPlayer()
|
VideoBackendPlayer()
|
||||||
@@ -61,6 +66,7 @@ struct VideoBackendPlayer
|
|||||||
return *(const T*)BackendState;
|
return *(const T*)BackendState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Created(const VideoBackendPlayerInfo& info);
|
||||||
void InitVideoFrame();
|
void InitVideoFrame();
|
||||||
void UpdateVideoFrame(Span<byte> data, TimeSpan time, TimeSpan duration);
|
void UpdateVideoFrame(Span<byte> data, TimeSpan time, TimeSpan duration);
|
||||||
void UpdateAudioBuffer(Span<byte> data, TimeSpan time, TimeSpan duration);
|
void UpdateAudioBuffer(Span<byte> data, TimeSpan time, TimeSpan duration);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "VideoBackend.h"
|
#include "VideoBackend.h"
|
||||||
#include "Engine/Audio/AudioBackend.h"
|
#include "Engine/Audio/AudioBackend.h"
|
||||||
#include "Engine/Core/Log.h"
|
#include "Engine/Core/Log.h"
|
||||||
|
#include "Engine/Core/Math/Quaternion.h"
|
||||||
#include "Engine/Profiler/ProfilerCPU.h"
|
#include "Engine/Profiler/ProfilerCPU.h"
|
||||||
#include "Engine/Engine/EngineService.h"
|
#include "Engine/Engine/EngineService.h"
|
||||||
#include "Engine/Graphics/GPUDevice.h"
|
#include "Engine/Graphics/GPUDevice.h"
|
||||||
@@ -50,6 +51,8 @@ protected:
|
|||||||
GPUTexture* frame = _player->Frame;
|
GPUTexture* frame = _player->Frame;
|
||||||
if (!frame->IsAllocated())
|
if (!frame->IsAllocated())
|
||||||
return Result::MissingResources;
|
return Result::MissingResources;
|
||||||
|
PROFILE_CPU();
|
||||||
|
ZoneText(_player->DebugUrl, _player->DebugUrlLen);
|
||||||
|
|
||||||
if (PixelFormatExtensions::IsVideo(_player->Format))
|
if (PixelFormatExtensions::IsVideo(_player->Format))
|
||||||
{
|
{
|
||||||
@@ -178,6 +181,15 @@ bool Video::CreatePlayerBackend(const VideoBackendPlayerInfo& info, VideoBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VideoBackendPlayer::Created(const VideoBackendPlayerInfo& info)
|
||||||
|
{
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
DebugUrlLen = info.Url.Length();
|
||||||
|
DebugUrl = (Char*)Allocator::Allocate(DebugUrlLen * sizeof(Char) + 2);
|
||||||
|
Platform::MemoryCopy(DebugUrl, *info.Url, DebugUrlLen * 2 + 2);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void VideoBackendPlayer::InitVideoFrame()
|
void VideoBackendPlayer::InitVideoFrame()
|
||||||
{
|
{
|
||||||
if (!GPUDevice::Instance)
|
if (!GPUDevice::Instance)
|
||||||
@@ -189,6 +201,7 @@ void VideoBackendPlayer::InitVideoFrame()
|
|||||||
void VideoBackendPlayer::UpdateVideoFrame(Span<byte> data, TimeSpan time, TimeSpan duration)
|
void VideoBackendPlayer::UpdateVideoFrame(Span<byte> data, TimeSpan time, TimeSpan duration)
|
||||||
{
|
{
|
||||||
PROFILE_CPU();
|
PROFILE_CPU();
|
||||||
|
ZoneText(DebugUrl, DebugUrlLen);
|
||||||
VideoFrameTime = time;
|
VideoFrameTime = time;
|
||||||
VideoFrameDuration = duration;
|
VideoFrameDuration = duration;
|
||||||
if (!GPUDevice::Instance || GPUDevice::Instance->GetRendererType() == RendererType::Null)
|
if (!GPUDevice::Instance || GPUDevice::Instance->GetRendererType() == RendererType::Null)
|
||||||
@@ -238,32 +251,79 @@ void VideoBackendPlayer::UpdateVideoFrame(Span<byte> data, TimeSpan time, TimeSp
|
|||||||
void VideoBackendPlayer::UpdateAudioBuffer(Span<byte> data, TimeSpan time, TimeSpan duration)
|
void VideoBackendPlayer::UpdateAudioBuffer(Span<byte> data, TimeSpan time, TimeSpan duration)
|
||||||
{
|
{
|
||||||
PROFILE_CPU();
|
PROFILE_CPU();
|
||||||
|
ZoneText(DebugUrl, DebugUrlLen);
|
||||||
AudioBufferTime = time;
|
AudioBufferTime = time;
|
||||||
AudioBufferDuration = duration;
|
AudioBufferDuration = duration;
|
||||||
auto start = time.GetTotalMilliseconds();
|
|
||||||
auto dur = duration.GetTotalMilliseconds();
|
|
||||||
auto end = (time + duration).GetTotalMilliseconds();
|
|
||||||
if (!AudioBackend::Instance)
|
if (!AudioBackend::Instance)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Setup audio source
|
||||||
|
bool newSource = AudioSource == 0;
|
||||||
|
if (newSource)
|
||||||
|
{
|
||||||
|
// TODO: spatial video player
|
||||||
|
// TODO: video player volume/pan control
|
||||||
|
AudioSource = AudioBackend::Source::Add(AudioInfo, Vector3::Zero, Quaternion::Identity, 1.0f, 1.0f, 0.0f, false, false, 1.0f, 1000.0f, 1.0f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Get the processed buffers count
|
||||||
|
int32 numProcessedBuffers = 0;
|
||||||
|
AudioBackend::Source::GetProcessedBuffersCount(AudioSource, numProcessedBuffers);
|
||||||
|
if (numProcessedBuffers > 0)
|
||||||
|
{
|
||||||
|
// Unbind processed buffers from the source
|
||||||
|
AudioBackend::Source::DequeueProcessedBuffers(AudioSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get audio buffer
|
||||||
|
uint32 bufferId = AudioBuffers[NextAudioBuffer];
|
||||||
|
if (bufferId == 0)
|
||||||
|
{
|
||||||
|
bufferId = AudioBackend::Buffer::Create();
|
||||||
|
AudioBuffers[NextAudioBuffer] = bufferId;
|
||||||
|
}
|
||||||
|
NextAudioBuffer = (NextAudioBuffer + 1) % ARRAY_COUNT(AudioBuffers);
|
||||||
|
|
||||||
// Update audio buffer
|
// Update audio buffer
|
||||||
if (!AudioBuffer)
|
|
||||||
AudioBuffer = AudioBackend::Buffer::Create();
|
|
||||||
AudioDataInfo dataInfo = AudioInfo;
|
AudioDataInfo dataInfo = AudioInfo;
|
||||||
const uint32 samplesPerSecond = dataInfo.SampleRate * dataInfo.NumChannels;
|
const uint32 samplesPerSecond = dataInfo.SampleRate * dataInfo.NumChannels;
|
||||||
const uint32 maxSamplesInData = (uint32)data.Length() * 8 / dataInfo.BitDepth;
|
const uint32 maxSamplesInData = (uint32)data.Length() * 8 / dataInfo.BitDepth;
|
||||||
const uint32 maxSamplesInDuration = (uint32)Math::CeilToInt(samplesPerSecond * duration.GetTotalSeconds());
|
const uint32 maxSamplesInDuration = (uint32)Math::CeilToInt(samplesPerSecond * duration.GetTotalSeconds());
|
||||||
dataInfo.NumSamples = Math::Min(maxSamplesInData, maxSamplesInDuration);
|
dataInfo.NumSamples = Math::Min(maxSamplesInData, maxSamplesInDuration);
|
||||||
AudioBackend::Buffer::Write(AudioBuffer, data.Get(), dataInfo);
|
AudioBackend::Buffer::Write(bufferId, data.Get(), dataInfo);
|
||||||
|
|
||||||
|
// Append audio buffer
|
||||||
|
AudioBackend::Source::QueueBuffer(AudioSource, bufferId);
|
||||||
|
if (newSource)
|
||||||
|
{
|
||||||
|
AudioBackend::Source::Play(AudioSource);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoBackendPlayer::ReleaseResources()
|
void VideoBackendPlayer::ReleaseResources()
|
||||||
{
|
{
|
||||||
if (AudioBuffer)
|
if (AudioSource)
|
||||||
AudioBackend::Buffer::Delete(AudioBuffer);
|
{
|
||||||
|
AudioBackend::Source::Stop(AudioSource);
|
||||||
|
AudioBackend::Source::Remove(AudioSource);
|
||||||
|
AudioSource = 0;
|
||||||
|
}
|
||||||
|
for (uint32& bufferId : AudioBuffers)
|
||||||
|
{
|
||||||
|
if (bufferId)
|
||||||
|
{
|
||||||
|
AudioBackend::Buffer::Delete(bufferId);
|
||||||
|
bufferId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (UploadVideoFrameTask)
|
if (UploadVideoFrameTask)
|
||||||
UploadVideoFrameTask->Cancel();
|
UploadVideoFrameTask->Cancel();
|
||||||
VideoFrameMemory.Release();
|
VideoFrameMemory.Release();
|
||||||
SAFE_DELETE_GPU_RESOURCE(Frame);
|
SAFE_DELETE_GPU_RESOURCE(Frame);
|
||||||
SAFE_DELETE_GPU_RESOURCE(FrameUpload);
|
SAFE_DELETE_GPU_RESOURCE(FrameUpload);
|
||||||
|
#ifdef TRACY_ENABLE
|
||||||
|
Allocator::Free(DebugUrl);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user