Temp work
@@ -17,7 +17,18 @@
|
||||
"Bash(powershell:*)",
|
||||
"WebSearch",
|
||||
"Bash(start Vav1Player.exe)",
|
||||
"Bash(dotnet run:*)"
|
||||
"Bash(dotnet run:*)",
|
||||
"Bash(cmake:*)",
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" \"Vav2Player.vcxproj\" \"/p:Configuration=Release\" \"/p:Platform=x64\")",
|
||||
"Bash(msbuild:*)",
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" Vav2Player.vcxproj /p:Configuration=Debug /p:Platform=x64 /v:minimal)",
|
||||
"Read(//c/Program Files/Microsoft Visual Studio/2022/Community/MSBuild/Current/Bin/**)",
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" /noautoresponse Vav2Player.vcxproj /p:Configuration=Debug /p:Platform=x64)",
|
||||
"Bash(start Vav2Player.exe)",
|
||||
"Bash(\".\\Vav2Player.exe\" \"D:\\Project\\video-av1\\sample\\output.webm\")",
|
||||
"Bash(\"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\" Vav2Player.vcxproj /p:Configuration=Debug /p:Platform=x64)",
|
||||
"Bash(\".\\Vav2Player.exe\" \"test.webm\")",
|
||||
"Bash(\".\\Vav2Player.exe\" \"D:\\Project\\video-av1\\sample\\simple_test.webm\")"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
129
build_dav1d.bat
Normal file
@@ -0,0 +1,129 @@
|
||||
@echo off
|
||||
echo Building dav1d static library (Release + Debug) for win64...
|
||||
|
||||
REM Clean previous build
|
||||
echo Cleaning previous build...
|
||||
if exist lib\dav1d rmdir /S /Q lib\dav1d
|
||||
if exist include\dav1d rmdir /S /Q include\dav1d
|
||||
if exist oss\dav1d\build_static_release rmdir /S /Q oss\dav1d\build_static_release
|
||||
if exist oss\dav1d\build_static_debug rmdir /S /Q oss\dav1d\build_static_debug
|
||||
|
||||
REM Create output directories
|
||||
echo Creating output directories...
|
||||
mkdir lib\dav1d 2>nul
|
||||
mkdir include\dav1d 2>nul
|
||||
|
||||
REM =============================================================================
|
||||
REM Build Release version (STATIC)
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Building RELEASE static version of dav1d...
|
||||
echo ========================================
|
||||
|
||||
REM Create build directory
|
||||
cd oss\dav1d
|
||||
|
||||
REM Configure with Meson (Release Static)
|
||||
echo Configuring dav1d Release static build...
|
||||
meson setup build_static_release --buildtype=release --default-library=static --prefix="D:/Project/video-av1/build_output/release" -Denable_tools=false -Denable_tests=false -Denable_examples=false
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Meson Release static configuration failed!
|
||||
cd ..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build the library (Release)
|
||||
echo Building dav1d Release static...
|
||||
meson compile -C build_static_release
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Release static build failed!
|
||||
cd ..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM =============================================================================
|
||||
REM Build Debug version (STATIC)
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Building DEBUG static version of dav1d...
|
||||
echo ========================================
|
||||
|
||||
REM Configure with Meson (Debug Static)
|
||||
echo Configuring dav1d Debug static build...
|
||||
meson setup build_static_debug --buildtype=debug --default-library=static --prefix="D:/Project/video-av1/build_output/debug" -Denable_tools=false -Denable_tests=false -Denable_examples=false
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Meson Debug static configuration failed!
|
||||
cd ..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build the library (Debug)
|
||||
echo Building dav1d Debug static...
|
||||
meson compile -C build_static_debug
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Debug static build failed!
|
||||
cd ..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Go back to root directory
|
||||
cd ..\..
|
||||
|
||||
REM =============================================================================
|
||||
REM Install header files FIRST (to ensure headers are copied even if libraries fail)
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo Installing header files...
|
||||
xcopy /E /I /Y "oss\dav1d\include\dav1d\*" "include\dav1d\"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo WARNING: Failed to copy header files, but continuing...
|
||||
) else (
|
||||
echo Successfully copied dav1d headers
|
||||
)
|
||||
|
||||
REM Copy generated version header
|
||||
echo Copying generated version header...
|
||||
copy "oss\dav1d\build_static_release\include\vcs_version.h" "include\dav1d\"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo WARNING: Failed to copy vcs_version.h, but continuing...
|
||||
) else (
|
||||
echo Successfully copied vcs_version.h
|
||||
)
|
||||
|
||||
REM =============================================================================
|
||||
REM Install static library files
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo Installing static library files...
|
||||
|
||||
REM Copy Release static library
|
||||
echo Copying Release static library...
|
||||
copy "oss\dav1d\build_static_release\src\libdav1d.a" "lib\dav1d\dav1d.lib"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Failed to copy Release static library!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Copy Debug static library (with -debug postfix)
|
||||
echo Copying Debug static library...
|
||||
copy "oss\dav1d\build_static_debug\src\libdav1d.a" "lib\dav1d\dav1d-debug.lib"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Failed to copy Debug static library!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo dav1d static build completed successfully!
|
||||
echo ========================================
|
||||
echo Release Static Library:
|
||||
echo - lib\dav1d\dav1d.lib
|
||||
echo Debug Static Library:
|
||||
echo - lib\dav1d\dav1d-debug.lib
|
||||
echo Headers: include\dav1d\
|
||||
echo.
|
||||
echo NOTE: Static libraries are now integrated into the final executable.
|
||||
echo No DLL files are needed for runtime distribution.
|
||||
echo.
|
||||
159
build_libwebm.bat
Normal file
@@ -0,0 +1,159 @@
|
||||
@echo off
|
||||
echo Building libwebm library (Release + Debug) for win64...
|
||||
|
||||
REM Clean previous build
|
||||
echo Cleaning previous build...
|
||||
if exist lib\libwebm rmdir /S /Q lib\libwebm
|
||||
if exist include\libwebm rmdir /S /Q include\libwebm
|
||||
if exist oss\libwebm\build_win64 rmdir /S /Q oss\libwebm\build_win64
|
||||
if exist oss\libwebm\build_debug rmdir /S /Q oss\libwebm\build_debug
|
||||
|
||||
REM Create output directories
|
||||
echo Creating output directories...
|
||||
mkdir lib\libwebm 2>nul
|
||||
mkdir include\libwebm 2>nul
|
||||
|
||||
REM =============================================================================
|
||||
REM Build Release version
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Building RELEASE version of libwebm...
|
||||
echo ========================================
|
||||
|
||||
REM Create build directory
|
||||
cd oss\libwebm
|
||||
mkdir build_win64 2>nul
|
||||
cd build_win64
|
||||
|
||||
REM Configure with CMake (Release)
|
||||
echo Configuring libwebm Release build...
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DENABLE_TESTS=OFF -DENABLE_SAMPLE_PROGRAMS=OFF -DCMAKE_INSTALL_PREFIX="D:/Project/video-av1" ..
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo CMake Release configuration failed!
|
||||
cd ..\..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build the library (Release)
|
||||
echo Building libwebm Release...
|
||||
cmake --build . --config Release
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Release build failed!
|
||||
cd ..\..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Go back to libwebm source directory
|
||||
cd ..
|
||||
|
||||
REM =============================================================================
|
||||
REM Build Debug version
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo ========================================
|
||||
echo Building DEBUG version of libwebm...
|
||||
echo ========================================
|
||||
|
||||
REM Create debug build directory
|
||||
mkdir build_debug 2>nul
|
||||
cd build_debug
|
||||
|
||||
REM Configure with CMake (Debug)
|
||||
echo Configuring libwebm Debug build...
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DENABLE_TESTS=OFF -DENABLE_SAMPLE_PROGRAMS=OFF ..
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo CMake Debug configuration failed!
|
||||
cd ..\..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build the library (Debug)
|
||||
echo Building libwebm Debug...
|
||||
cmake --build . --config Debug
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Debug build failed!
|
||||
cd ..\..\..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Rename debug library immediately after build
|
||||
echo Renaming debug library...
|
||||
if exist Debug\webm.lib (
|
||||
ren Debug\webm.lib webm-debug.lib
|
||||
echo Renamed webm.lib to webm-debug.lib
|
||||
) else (
|
||||
echo WARNING: webm.lib not found in debug build!
|
||||
)
|
||||
|
||||
REM Go back to root directory
|
||||
cd ..\..\.
|
||||
|
||||
REM =============================================================================
|
||||
REM Install header files FIRST (to ensure headers are copied even if libraries fail)
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo Installing header files...
|
||||
copy "oss\libwebm\*.hpp" "include\libwebm\" 2>nul
|
||||
echo Copied root header files
|
||||
|
||||
xcopy /E /I /Y "oss\libwebm\mkvmuxer" "include\libwebm\mkvmuxer"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo WARNING: Failed to copy mkvmuxer headers, but continuing...
|
||||
) else (
|
||||
echo Successfully copied mkvmuxer headers
|
||||
)
|
||||
|
||||
xcopy /E /I /Y "oss\libwebm\mkvparser" "include\libwebm\mkvparser"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo WARNING: Failed to copy mkvparser headers, but continuing...
|
||||
) else (
|
||||
echo Successfully copied mkvparser headers
|
||||
)
|
||||
|
||||
xcopy /E /I /Y "oss\libwebm\common" "include\libwebm\common"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo WARNING: Failed to copy common headers, but continuing...
|
||||
) else (
|
||||
echo Successfully copied common headers
|
||||
)
|
||||
|
||||
xcopy /E /I /Y "oss\libwebm\webvtt" "include\libwebm\webvtt"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo WARNING: Failed to copy webvtt headers, but continuing...
|
||||
) else (
|
||||
echo Successfully copied webvtt headers
|
||||
)
|
||||
|
||||
REM =============================================================================
|
||||
REM Install library files
|
||||
REM =============================================================================
|
||||
echo.
|
||||
echo Installing library files...
|
||||
|
||||
REM Copy Release library
|
||||
echo Copying Release library...
|
||||
copy "oss\libwebm\build_win64\Release\webm.lib" "lib\libwebm\"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Failed to copy Release library!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Copy Debug library (already renamed)
|
||||
echo Copying Debug library...
|
||||
copy "oss\libwebm\build_debug\Debug\webm-debug.lib" "lib\libwebm\"
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Failed to copy Debug library!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo libwebm build completed successfully!
|
||||
echo ========================================
|
||||
echo Release Library:
|
||||
echo - lib\libwebm\webm.lib
|
||||
echo Debug Library:
|
||||
echo - lib\libwebm\webm-debug.lib
|
||||
echo Headers: include\libwebm\
|
||||
echo.
|
||||
14
test_headless.bat
Normal file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
echo Testing Vav2Player headless mode...
|
||||
echo.
|
||||
|
||||
cd "vav2\Vav2Player\x64\Debug\Vav2Player"
|
||||
|
||||
echo Running: Vav2Player.exe sample\output.webm
|
||||
echo.
|
||||
|
||||
"Vav2Player.exe" "D:\Project\video-av1\sample\output.webm"
|
||||
|
||||
echo.
|
||||
echo Test completed.
|
||||
pause
|
||||
@@ -95,6 +95,8 @@ namespace Vav1Player.Tests.WebM
|
||||
_output.WriteLine($"Expected total samples: {reader.TotalSamples}");
|
||||
|
||||
// Initialize decoder with sequence header from CodecPrivate if available
|
||||
byte[]? sequenceOBU = null;
|
||||
|
||||
if (trackInfo!.Av1ConfigurationRecord != null)
|
||||
{
|
||||
_output.WriteLine($"Found CodecPrivate data: {trackInfo.Av1ConfigurationRecord.Length} bytes");
|
||||
@@ -105,7 +107,6 @@ namespace Vav1Player.Tests.WebM
|
||||
// Analysis shows CodecPrivate: `81 0C 0C 00 0A 0F 00 00 00 62 EF BF E1 BD DA F8`
|
||||
// Frame 1 starts with: `0A 0F 00 00 00 62 EF BF E1 BD DA F8`
|
||||
// So the actual sequence header is at offset 4!
|
||||
byte[]? sequenceOBU = null;
|
||||
for (int i = 0; i < Math.Min(10, trackInfo.Av1ConfigurationRecord.Length); i++)
|
||||
{
|
||||
byte currentByte = trackInfo.Av1ConfigurationRecord[i];
|
||||
@@ -156,12 +157,22 @@ namespace Vav1Player.Tests.WebM
|
||||
var sequenceHex = string.Join(" ", sequenceOBU.Take(Math.Min(16, sequenceOBU.Length)).Select(b => b.ToString("X2")));
|
||||
_output.WriteLine($"Using sequence OBU for init: {sequenceHex} (length: {sequenceOBU.Length})");
|
||||
|
||||
_output.WriteLine("About to initialize decoder with sequence OBU...");
|
||||
var initResult = _decoder.DecodeFrame(sequenceOBU, out var _);
|
||||
_output.WriteLine($"Decoder initialization with extracted sequence OBU: {(initResult ? "SUCCESS" : "FAILED")}");
|
||||
|
||||
if (!initResult)
|
||||
{
|
||||
_output.WriteLine("CodecPrivate sequence header failed, will try using first frame's sequence header");
|
||||
_output.WriteLine("Attempting to get more detailed error information...");
|
||||
// Try to decode a small part to get error details
|
||||
var testResult = _decoder.DecodeFrame(new byte[] { 0x0A, 0x01, 0x00 }, out var _);
|
||||
_output.WriteLine($"Test decode result: {testResult}");
|
||||
}
|
||||
|
||||
if (!initResult)
|
||||
{
|
||||
_output.WriteLine("CodecPrivate sequence header is invalid, will extract from first frame instead");
|
||||
sequenceOBU = null; // Clear invalid sequence header
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -174,6 +185,12 @@ namespace Vav1Player.Tests.WebM
|
||||
_output.WriteLine("No CodecPrivate data found");
|
||||
}
|
||||
|
||||
// If CodecPrivate sequence header failed, extract from first frame
|
||||
if (sequenceOBU == null)
|
||||
{
|
||||
_output.WriteLine("Will extract sequence header from first keyframe");
|
||||
}
|
||||
|
||||
// Act - Decode each frame one by one
|
||||
bool firstFrameUsedForInit = false;
|
||||
while (reader.HasMoreData)
|
||||
@@ -197,38 +214,37 @@ namespace Vav1Player.Tests.WebM
|
||||
var hexData = string.Join(" ", chunk.Data.Take(Math.Min(16, chunk.Data.Length)).Select(b => b.ToString("X2")));
|
||||
_output.WriteLine($" First bytes: [{hexData}]");
|
||||
|
||||
// Special handling for first frame if it contains sequence header
|
||||
// Special handling for first frame - try direct decoding without separate initialization
|
||||
if (totalFrames == 1 && chunk.IsKeyFrame && !firstFrameUsedForInit)
|
||||
{
|
||||
// Check if frame starts with sequence header (OBU type 1)
|
||||
if (chunk.Data.Length > 0 && ((chunk.Data[0] >> 3) & 0xF) == 1)
|
||||
{
|
||||
_output.WriteLine(" First frame contains sequence header, using for decoder initialization");
|
||||
var initResult = _decoder.DecodeFrame(chunk.Data, out var initFrame);
|
||||
if (initResult)
|
||||
{
|
||||
_output.WriteLine(" ✓ Decoder initialization with first frame: SUCCESS");
|
||||
if (initFrame.HasValue)
|
||||
{
|
||||
decodedFrames++;
|
||||
var frame = initFrame.Value;
|
||||
_output.WriteLine($" ✓ Also decoded frame: {frame.Width}x{frame.Height}, Layout={frame.PixelLayout}");
|
||||
frame.Release();
|
||||
}
|
||||
firstFrameUsedForInit = true;
|
||||
_output.WriteLine(" First keyframe - attempting direct decode (letting dav1d handle initialization internally)");
|
||||
|
||||
// Limit test to prevent excessive output
|
||||
if (totalFrames >= 50)
|
||||
{
|
||||
_output.WriteLine($"Stopping at 50 frames for test performance...");
|
||||
break;
|
||||
}
|
||||
continue; // Skip normal decoding logic for this frame
|
||||
}
|
||||
else
|
||||
// Try decoding the entire first keyframe directly
|
||||
// dav1d should handle sequence header initialization internally
|
||||
var frameResult = _decoder.DecodeFrame(chunk.Data, out var decodedFrame);
|
||||
if (frameResult && decodedFrame.HasValue)
|
||||
{
|
||||
decodedFrames++;
|
||||
var frame = decodedFrame.Value;
|
||||
_output.WriteLine($" ✓ Successfully decoded first keyframe: {frame.Width}x{frame.Height}, Layout={frame.PixelLayout}");
|
||||
frame.Release();
|
||||
firstFrameUsedForInit = true;
|
||||
|
||||
// Limit test to prevent excessive output
|
||||
if (totalFrames >= 50)
|
||||
{
|
||||
_output.WriteLine(" ✗ Decoder initialization with first frame: FAILED");
|
||||
_output.WriteLine($"Stopping at 50 frames for test performance...");
|
||||
break;
|
||||
}
|
||||
continue; // Skip normal decoding logic for this frame
|
||||
}
|
||||
else
|
||||
{
|
||||
_output.WriteLine(" ✗ Failed to decode first keyframe directly");
|
||||
_output.WriteLine(" This suggests the WebM data format may not be compatible with dav1d");
|
||||
|
||||
// Log frame structure for debugging
|
||||
LogFrameAnalysis(chunk.Data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +391,45 @@ namespace Vav1Player.Tests.WebM
|
||||
}
|
||||
}
|
||||
|
||||
private byte[]? ExtractSequenceHeaderFromFrame(byte[] frameData)
|
||||
{
|
||||
if (frameData == null || frameData.Length < 3) return null;
|
||||
|
||||
// Parse the first OBU to extract sequence header
|
||||
int position = 0;
|
||||
byte obuHeader = frameData[position++];
|
||||
|
||||
int obuType = (obuHeader >> 3) & 0xF;
|
||||
bool hasSizeField = (obuHeader & 0x02) != 0;
|
||||
|
||||
if (obuType != 1) return null; // Not a sequence header
|
||||
|
||||
if (hasSizeField && position < frameData.Length)
|
||||
{
|
||||
// Read LEB128 size field
|
||||
uint size = 0;
|
||||
int shift = 0;
|
||||
while (position < frameData.Length && shift < 35)
|
||||
{
|
||||
byte b = frameData[position++];
|
||||
size |= (uint)(b & 0x7F) << shift;
|
||||
if ((b & 0x80) == 0) break;
|
||||
shift += 7;
|
||||
}
|
||||
|
||||
// Extract the complete sequence header OBU
|
||||
int totalOBUSize = position + (int)size;
|
||||
if (totalOBUSize <= frameData.Length)
|
||||
{
|
||||
byte[] sequenceOBU = new byte[totalOBUSize];
|
||||
Array.Copy(frameData, 0, sequenceOBU, 0, totalOBUSize);
|
||||
return sequenceOBU;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string GetObuTypeName(int obuType)
|
||||
{
|
||||
return obuType switch
|
||||
|
||||
@@ -82,11 +82,15 @@ namespace Vav1Player.Decoder
|
||||
result = Dav1dNative.dav1d_send_data(_context, ref dav1dData);
|
||||
if (result != 0 && result != -11) // -11 is EAGAIN (need more data)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Dav1dDecoder] dav1d_send_data failed with error: {result}");
|
||||
System.Diagnostics.Debug.WriteLine($"[Dav1dDecoder] Data size: {data.Length} bytes");
|
||||
// Log first few bytes for debugging
|
||||
var hexData = string.Join(" ", data.Take(Math.Min(16, data.Length)).Select(b => b.ToString("X2")));
|
||||
System.Diagnostics.Debug.WriteLine($"[Dav1dDecoder] Data prefix: {hexData}");
|
||||
string errorMsg = $"[Dav1dDecoder] dav1d_send_data failed with error: {result}";
|
||||
errorMsg += $"\n[Dav1dDecoder] Data size: {data.Length} bytes";
|
||||
var hexData = string.Join(" ", data.Take(Math.Min(32, data.Length)).Select(b => b.ToString("X2")));
|
||||
errorMsg += $"\n[Dav1dDecoder] Data prefix: {hexData}";
|
||||
errorMsg += $"\n[Dav1dDecoder] Error details: {Dav1dErrorCodes.GetErrorDescription(result)}";
|
||||
|
||||
System.Diagnostics.Debug.WriteLine(errorMsg);
|
||||
Console.WriteLine(errorMsg); // Also print to console so it shows in test output
|
||||
|
||||
Dav1dNative.dav1d_data_unref(ref dav1dData);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ namespace Vav1Player.Video
|
||||
|
||||
bool hasSequenceHeader = false;
|
||||
bool hasFrameHeader = false;
|
||||
bool hasFrameOBU = false;
|
||||
int obuCount = 0;
|
||||
|
||||
// Parse OBUs in the keyframe using proper AV1 spec parsing
|
||||
@@ -249,6 +250,7 @@ namespace Vav1Player.Video
|
||||
// Track important OBU types for spec validation
|
||||
if (obuInfo.Value.obuType == 1) hasSequenceHeader = true;
|
||||
if (obuInfo.Value.obuType == 3) hasFrameHeader = true;
|
||||
if (obuInfo.Value.obuType == 6) hasFrameOBU = true;
|
||||
|
||||
// Advance to next OBU
|
||||
position = obuInfo.Value.nextPosition;
|
||||
@@ -260,12 +262,13 @@ namespace Vav1Player.Video
|
||||
System.Diagnostics.Debug.WriteLine($" ⚠️ SPEC VIOLATION: Keyframe at sample {sampleIndex} missing Sequence Header OBU");
|
||||
}
|
||||
|
||||
if (!hasFrameHeader)
|
||||
// Either Frame Header (Type 3) OR Frame OBU (Type 6) is required, not both
|
||||
if (!hasFrameHeader && !hasFrameOBU)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($" ⚠️ SPEC VIOLATION: Keyframe at sample {sampleIndex} missing Frame Header OBU");
|
||||
System.Diagnostics.Debug.WriteLine($" ⚠️ SPEC VIOLATION: Keyframe at sample {sampleIndex} missing Frame Header (Type 3) or Frame OBU (Type 6)");
|
||||
}
|
||||
|
||||
if (hasSequenceHeader && hasFrameHeader)
|
||||
if (hasSequenceHeader && (hasFrameHeader || hasFrameOBU))
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($" ✅ Keyframe at sample {sampleIndex} appears spec compliant");
|
||||
}
|
||||
@@ -343,6 +346,37 @@ namespace Vav1Player.Video
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<byte[]> SplitOBUsFromData(byte[] data)
|
||||
{
|
||||
var obuList = new List<byte[]>();
|
||||
int position = 0;
|
||||
|
||||
while (position < data.Length)
|
||||
{
|
||||
var obuInfo = ParseObuHeader(data, position);
|
||||
if (obuInfo == null) break;
|
||||
|
||||
int obuStart = position;
|
||||
int obuEnd = obuInfo.Value.nextPosition;
|
||||
|
||||
// Extract this OBU data including header and payload
|
||||
int obuLength = obuEnd - obuStart;
|
||||
if (obuLength > 0 && obuEnd <= data.Length)
|
||||
{
|
||||
byte[] obuData = new byte[obuLength];
|
||||
Array.Copy(data, obuStart, obuData, 0, obuLength);
|
||||
obuList.Add(obuData);
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[SplitOBUs] Extracted OBU Type {obuInfo.Value.obuType}: {obuLength} bytes");
|
||||
}
|
||||
|
||||
position = obuEnd;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[SplitOBUs] Split data into {obuList.Count} OBUs");
|
||||
return obuList;
|
||||
}
|
||||
|
||||
private string GetOBUTypeName(int obuType)
|
||||
{
|
||||
return obuType switch
|
||||
@@ -542,13 +576,67 @@ namespace Vav1Player.Video
|
||||
else
|
||||
{
|
||||
// WebM/MKV: Use data directly as AV1 OBUs in "Low Overhead Bitstream Format"
|
||||
decodingData = chunk.Data;
|
||||
System.Diagnostics.Debug.WriteLine($"[VideoDecoderPipeline] Matroska: Using data directly, size: {decodingData.Length}");
|
||||
System.Diagnostics.Debug.WriteLine($"[VideoDecoderPipeline] Matroska: Using data directly, size: {chunk.Data.Length}");
|
||||
|
||||
// For WebM keyframes, validate spec compliance
|
||||
if (chunk.IsKeyFrame)
|
||||
// For WebM keyframes with multiple OBUs, handle them separately
|
||||
if (chunk.IsKeyFrame && chunk.Data.Length > 1000) // Large keyframes likely have multiple OBUs
|
||||
{
|
||||
ValidateAv1Keyframe(chunk.Data, chunk.SampleIndex);
|
||||
|
||||
// Split OBUs and process separately for dav1d
|
||||
var obuList = SplitOBUsFromData(chunk.Data);
|
||||
bool decodingSuccess = false;
|
||||
|
||||
foreach (var obuData in obuList)
|
||||
{
|
||||
if (obuData.Length > 0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[VideoDecoderPipeline] Processing individual OBU: {obuData.Length} bytes");
|
||||
if (_decoder.DecodeFrame(obuData, out var frameResult))
|
||||
{
|
||||
if (frameResult.HasValue)
|
||||
{
|
||||
var frame = frameResult.Value;
|
||||
System.Diagnostics.Debug.WriteLine($"[VideoDecoderPipeline] Decoded frame #{_frameCounter}: {frame.Width}x{frame.Height}");
|
||||
|
||||
// Create video frame with timing information
|
||||
var videoFrame = new VideoFrame(frame, chunk.PresentationTimeMs, _frameCounter, chunk.IsKeyFrame);
|
||||
|
||||
// Add to frame buffer
|
||||
var enqueued = await _frameBuffer.TryEnqueueAsync(videoFrame, _cancellationTokenSource.Token);
|
||||
if (enqueued)
|
||||
{
|
||||
_frameCounter++;
|
||||
decodingSuccess = true;
|
||||
|
||||
// Log buffer status periodically
|
||||
if (_frameCounter % 10 == 0)
|
||||
{
|
||||
var stats = _frameBuffer.GetStats();
|
||||
System.Diagnostics.Debug.WriteLine($"[VideoDecoderPipeline] Buffer: {stats}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Buffer is full, dispose the frame
|
||||
videoFrame.Dispose();
|
||||
System.Diagnostics.Debug.WriteLine("[VideoDecoderPipeline] Buffer full, dropped frame");
|
||||
}
|
||||
break; // Successfully decoded, stop processing more OBUs
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!decodingSuccess)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[VideoDecoderPipeline] Failed to decode keyframe with split OBUs");
|
||||
}
|
||||
return; // Skip regular decoding for keyframes
|
||||
}
|
||||
else
|
||||
{
|
||||
decodingData = chunk.Data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,6 +183,7 @@ namespace Vav1Player.Video
|
||||
public int Size { get; init; }
|
||||
public long PresentationTimeMs { get; init; }
|
||||
public bool IsKeyFrame { get; init; }
|
||||
public byte[]? Data { get; init; } // Pre-extracted pure AV1 data
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -388,7 +389,8 @@ namespace Vav1Player.Video
|
||||
Offset = block.Offset,
|
||||
Size = block.Size,
|
||||
PresentationTimeMs = (long)block.Timestamp,
|
||||
IsKeyFrame = block.IsKeyFrame
|
||||
IsKeyFrame = block.IsKeyFrame,
|
||||
Data = block.Data // Include pre-extracted pure AV1 data
|
||||
}).ToList();
|
||||
|
||||
// Estimate frame rate from timestamps
|
||||
@@ -432,18 +434,36 @@ namespace Vav1Player.Video
|
||||
try
|
||||
{
|
||||
var block = _blocks[(int)chunkIndex];
|
||||
_stream.Position = block.Offset;
|
||||
|
||||
var buffer = new byte[block.Size];
|
||||
var bytesRead = await _stream.ReadAsync(buffer, 0, block.Size, cancellationToken);
|
||||
|
||||
if (bytesRead != block.Size)
|
||||
// Use pre-extracted pure AV1 data from MatroskaParser instead of re-reading from file
|
||||
// This ensures we only pass pure AV1 bitstream to the decoder, not WebM container data
|
||||
if (block.Data != null && block.Data.Length > 0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Expected {block.Size} bytes, got {bytesRead}");
|
||||
return null;
|
||||
}
|
||||
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] ✅ Using pre-extracted pure AV1 data: {block.Data.Length} bytes (was {block.Size} in container)");
|
||||
Console.WriteLine($"[AV1_EXTRACT] Using pure AV1 data: {block.Data.Length} bytes (container size: {block.Size})");
|
||||
|
||||
return new VideoDataChunk(buffer, block.PresentationTimeMs, block.IsKeyFrame, chunkIndex, block.Offset);
|
||||
var hexData = string.Join(" ", block.Data.Take(16).Select(b => b.ToString("X2")));
|
||||
Console.WriteLine($"[AV1_EXTRACT] Pure AV1 data starts with: {hexData}");
|
||||
|
||||
return new VideoDataChunk(block.Data, block.PresentationTimeMs, block.IsKeyFrame, chunkIndex, block.Offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to file reading if Data is not available
|
||||
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Fallback: reading from file at offset {block.Offset}");
|
||||
_stream.Position = block.Offset;
|
||||
|
||||
var buffer = new byte[block.Size];
|
||||
var bytesRead = await _stream.ReadAsync(buffer, 0, block.Size, cancellationToken);
|
||||
|
||||
if (bytesRead != block.Size)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Expected {block.Size} bytes, got {bytesRead}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new VideoDataChunk(buffer, block.PresentationTimeMs, block.IsKeyFrame, chunkIndex, block.Offset);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -476,20 +496,21 @@ namespace Vav1Player.Video
|
||||
|
||||
private MatroskaParser CreateOptimizedMatroskaParser()
|
||||
{
|
||||
// For Matroska, limit memory usage to 10MB for metadata parsing
|
||||
const int maxMetadataSize = 10 * 1024 * 1024; // 10MB limit
|
||||
var readSize = Math.Min((int)_stream.Length, maxMetadataSize);
|
||||
// For Matroska, we need to read the entire file to get correct offsets
|
||||
// The previous approach of reading only 10MB caused offset misalignment
|
||||
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Reading entire WebM file for accurate parsing ({_stream.Length} bytes)");
|
||||
|
||||
var buffer = new byte[readSize];
|
||||
var buffer = new byte[_stream.Length];
|
||||
_stream.Position = 0;
|
||||
var totalRead = 0;
|
||||
while (totalRead < readSize)
|
||||
while (totalRead < _stream.Length)
|
||||
{
|
||||
var bytesRead = _stream.Read(buffer, totalRead, readSize - totalRead);
|
||||
var bytesRead = _stream.Read(buffer, totalRead, (int)_stream.Length - totalRead);
|
||||
if (bytesRead == 0) break;
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
|
||||
System.Diagnostics.Debug.WriteLine($"[StreamingMatroskaParser] Successfully read {totalRead} bytes for parsing");
|
||||
return new MatroskaParser(buffer);
|
||||
}
|
||||
|
||||
|
||||
252
vav2/CLAUDE.md
Normal file
@@ -0,0 +1,252 @@
|
||||
# Vav2Player - AV1 Video Player 개발 프로젝트
|
||||
|
||||
## 프로젝트 개요
|
||||
WinUI 3 C++로 작성된 AV1 파일 재생 플레이어
|
||||
- 목적: WebM/MKV 형식의 AV1 비디오 파일을 실시간으로 디코딩하여 재생
|
||||
- 현재 단계: 파일 출력 기반 스트리밍 파이프라인 구현 (렌더링은 추후)
|
||||
- 목표 성능: 30fps 끊김없는 실시간 재생
|
||||
|
||||
## 프로젝트 구조
|
||||
```
|
||||
D:\Project\video-av1\
|
||||
├── vav2/
|
||||
│ └── Vav2Player/ # WinUI 3 C++ 프로젝트 루트
|
||||
│ ├── Vav2Player.sln # Visual Studio 솔루션
|
||||
│ └── Vav2Player/ # 실제 프로젝트 폴더
|
||||
│ ├── Vav2Player.vcxproj # 프로젝트 파일
|
||||
│ ├── pch.h / pch.cpp # 미리 컴파일된 헤더
|
||||
│ ├── App.xaml.* # WinUI 앱 진입점
|
||||
│ └── MainWindow.xaml.* # 메인 윈도우
|
||||
├── include/
|
||||
│ ├── libwebm/ # libwebm 헤더 (mkvparser, mkvmuxer)
|
||||
│ └── dav1d/ # dav1d 헤더 (dav1d.h, picture.h 등)
|
||||
└── lib/
|
||||
├── libwebm/webm.lib # libwebm 정적 라이브러리 (x64)
|
||||
└── dav1d/ # dav1d 동적 라이브러리 (x64)
|
||||
├── dav1d.dll
|
||||
└── dav1d.lib
|
||||
```
|
||||
|
||||
## 전체 아키텍처 설계
|
||||
|
||||
### 데이터 플로우
|
||||
```
|
||||
[AV1 파일] → [libwebm Parser] → [AV1 Packet Queue] → [dav1d Decoder] → [YUV Frame Queue] → [File Output]
|
||||
↓ ↓ ↓
|
||||
[File Reader Thread] [Decoder Thread] [Output Thread]
|
||||
```
|
||||
|
||||
### 핵심 컴포넌트
|
||||
1. **WebMFileReader**: libwebm 기반 파일 파싱
|
||||
2. **AV1Decoder**: dav1d 기반 프레임 디코딩
|
||||
3. **StreamingPipeline**: 멀티스레드 스트리밍 관리
|
||||
4. **FileOutput**: Raw/BMP 파일 출력
|
||||
|
||||
## 구현 단계별 계획
|
||||
|
||||
### ✅ 완료된 작업
|
||||
- [x] 프로젝트 구조 분석
|
||||
- [x] libwebm/dav1d 라이브러리 의존성 확인
|
||||
- [x] 전체 아키텍처 설계
|
||||
|
||||
### 📋 구현 단계
|
||||
|
||||
#### 1단계: libwebm 기반 파일 로더 구현
|
||||
**목표**: WebM/MKV 파일을 파싱하여 AV1 비디오 트랙 추출
|
||||
**구현 파일**: `WebMFileReader.h/cpp`
|
||||
**기능**:
|
||||
- WebM/MKV 파일 열기 및 검증
|
||||
- 비디오 트랙 메타데이터 추출 (해상도, FPS, 코덱 정보)
|
||||
- AV1 트랙 식별 및 선택
|
||||
- 프레임별 패킷 추출 인터페이스
|
||||
- 시간 기반 탐색 지원
|
||||
|
||||
#### 2단계: dav1d 디코더 래퍼 구현
|
||||
**목표**: AV1 패킷을 YUV 프레임으로 디코딩
|
||||
**구현 파일**: `AV1Decoder.h/cpp`
|
||||
**기능**:
|
||||
- dav1d 컨텍스트 초기화/해제
|
||||
- AV1 패킷 입력 및 YUV 프레임 출력
|
||||
- 프레임 메타데이터 관리 (타임스탬프, 프레임 타입)
|
||||
- 에러 핸들링 및 복구
|
||||
- 메모리 관리 최적화
|
||||
|
||||
#### 3단계: 스트리밍 파이프라인 및 버퍼링 시스템 구현
|
||||
**목표**: 30fps 실시간 재생을 위한 멀티스레드 파이프라인
|
||||
**구현 파일**: `StreamingPipeline.h/cpp`, `FrameBuffer.h/cpp`
|
||||
**기능**:
|
||||
- Producer-Consumer 멀티스레드 구조
|
||||
- 프레임 버퍼 관리 (기본: 15프레임 = 0.5초 버퍼링)
|
||||
- 타이밍 제어 (30fps 기준 33.33ms 간격)
|
||||
- 백프레셔 핸들링 (버퍼 풀/빈 상태 처리)
|
||||
- 성능 모니터링 (FPS, 드롭된 프레임 수)
|
||||
|
||||
#### 4단계: Raw 및 BMP 파일 출력 기능 구현
|
||||
**목표**: 디코딩된 프레임을 파일로 저장
|
||||
**구현 파일**: `FileOutput.h/cpp`
|
||||
**기능**:
|
||||
- Raw YUV420P 포맷 출력
|
||||
- YUV → RGB 변환
|
||||
- BMP 파일 생성 및 저장
|
||||
- 프레임 번호 기반 파일명 생성
|
||||
- 출력 디렉토리 관리
|
||||
|
||||
## 기술적 고려사항
|
||||
|
||||
### 성능 최적화
|
||||
- **버퍼링 전략**: 15프레임 (0.5초) 기본 버퍼, 설정 가능
|
||||
- **메모리 풀**: 프레임 재사용을 위한 메모리 풀 구현
|
||||
- **스레드 동기화**: lock-free 큐 사용 고려
|
||||
- **SIMD 최적화**: dav1d 내장 최적화 활용
|
||||
|
||||
### 에러 처리
|
||||
- 파일 포맷 오류 감지 및 복구
|
||||
- 디코딩 실패 시 프레임 스킵
|
||||
- 메모리 부족 시 버퍼 크기 동적 조정
|
||||
- 스레드 예외 전파 메커니즘
|
||||
|
||||
### 확장성
|
||||
- 플러그인 아키텍처 (다른 코덱 지원)
|
||||
- 설정 파일 기반 매개변수 조정
|
||||
- 로깅 및 디버깅 인프라
|
||||
- 단위 테스트 지원
|
||||
|
||||
## 빌드 설정
|
||||
- 플랫폼: x64 Windows
|
||||
- 컴파일러: MSVC v143 (Visual Studio 2022)
|
||||
- 언어 표준: C++17 이상
|
||||
- 런타임: Windows App SDK 1.8
|
||||
|
||||
### 라이브러리 링크 설정
|
||||
```xml
|
||||
<!-- 추가 포함 디렉터리 -->
|
||||
$(ProjectDir)..\..\include\libwebm;
|
||||
$(ProjectDir)..\..\include\dav1d;
|
||||
|
||||
<!-- 추가 라이브러리 디렉터리 -->
|
||||
$(ProjectDir)..\..\lib\libwebm;
|
||||
$(ProjectDir)..\..\lib\dav1d;
|
||||
|
||||
<!-- 추가 종속성 -->
|
||||
webm.lib;
|
||||
dav1d.lib;
|
||||
```
|
||||
|
||||
## 다음 작업
|
||||
1. **1단계 구현 시작**: WebMFileReader 클래스 구현
|
||||
2. **프로젝트 설정**: vcxproj 파일에 include/lib 경로 및 종속성 추가
|
||||
3. **기본 테스트**: 간단한 WebM 파일 열기 테스트
|
||||
|
||||
## 구현 완료 상황
|
||||
|
||||
### ✅ **완료된 작업들 (2025-09-19)**
|
||||
1. **프로젝트 구조 설계** - VP9 확장성을 고려한 인터페이스 기반 아키텍처
|
||||
2. **소스 디렉토리 구조 생성** - `src/{Common,Decoder,FileIO,Pipeline,Output}`
|
||||
3. **핵심 데이터 타입 구현** - `VideoTypes.h` (VideoFrame, VideoMetadata, VideoPacket)
|
||||
4. **디코더 인터페이스 구현** - `IVideoDecoder.h` (모든 코덱용 공통 인터페이스)
|
||||
5. **디코더 팩토리 구현** - `VideoDecoderFactory.h/.cpp` (코덱별 디코더 생성)
|
||||
6. **AV1Decoder 껍데기 구현** - `AV1Decoder.h/.cpp` (dav1d 연동 준비 완료)
|
||||
7. **빌드 시스템 통합** - vcxproj 파일 업데이트 및 빌드 성공 확인
|
||||
|
||||
### 📁 **생성된 파일 구조**
|
||||
```
|
||||
vav2/Vav2Player/Vav2Player/src/
|
||||
├── Common/
|
||||
│ └── VideoTypes.h # 기본 데이터 구조체들
|
||||
├── Decoder/
|
||||
│ ├── IVideoDecoder.h # 디코더 공통 인터페이스
|
||||
│ ├── VideoDecoderFactory.h/.cpp # 디코더 팩토리
|
||||
│ └── AV1Decoder.h/.cpp # AV1 디코더 (스텁 구현)
|
||||
├── FileIO/ # TODO: WebMFileReader
|
||||
├── Pipeline/ # TODO: StreamingPipeline
|
||||
└── Output/ # TODO: FileOutput
|
||||
```
|
||||
|
||||
### ✅ **WebMFileReader 구현 완료** (2025-09-19)
|
||||
**주요 기능**:
|
||||
- libwebm 기반 WebM/MKV 파일 파싱 ✅
|
||||
- 비디오 트랙 탐색 및 메타데이터 추출 ✅
|
||||
- AV1/VP9 코덱 식별 및 트랙 선택 ✅
|
||||
- 프레임별 패킷 읽기 (`ReadNextPacket()`) ✅
|
||||
- 시간/프레임 기반 탐색 (`SeekToTime()`, `SeekToFrame()`) ✅
|
||||
- 에러 처리 및 상태 관리 ✅
|
||||
|
||||
**구현된 핵심 메서드**:
|
||||
- `OpenFile()` - WebM 파일 열기 및 검증
|
||||
- `GetVideoTracks()` - 지원 비디오 트랙 목록
|
||||
- `SelectVideoTrack()` - 특정 트랙 선택
|
||||
- `ReadNextPacket()` - 다음 비디오 패킷 읽기
|
||||
- `SeekToFrame()` / `SeekToTime()` - 탐색 기능
|
||||
- `Reset()` - 파일 시작으로 되돌리기
|
||||
|
||||
### ✅ **AV1Decoder 구현 완료** (2025-09-19)
|
||||
**주요 기능**:
|
||||
- dav1d API 완전 연동 ✅
|
||||
- 실제 AV1 패킷 디코딩 (`DecodeFrame()`) ✅
|
||||
- YUV420P/422P/444P 픽셀 포맷 지원 ✅
|
||||
- Dav1dPicture → VideoFrame 변환 (`ConvertDav1dPicture()`) ✅
|
||||
- 메모리 관리 및 에러 처리 ✅
|
||||
- 통계 수집 및 성능 모니터링 ✅
|
||||
- 설정 가능한 디코더 옵션 (스레드 수, 그레인 필터 등) ✅
|
||||
|
||||
**구현된 핵심 메서드**:
|
||||
- `Initialize()` / `Cleanup()` - dav1d 컨텍스트 생명주기 관리
|
||||
- `DecodeFrame()` - AV1 패킷 → YUV 프레임 디코딩
|
||||
- `Reset()` / `Flush()` - 디코더 상태 초기화 및 지연 프레임 처리
|
||||
- `ConvertDav1dPicture()` - stride를 고려한 YUV 데이터 복사
|
||||
- `SetAV1Settings()` - AV1 전용 설정 관리
|
||||
|
||||
### ✅ **통합 테스트 완료** (2025-09-19)
|
||||
**테스트 파일**: `src/TestMain.cpp` / `src/TestMain.h`
|
||||
**기능**: WebMFileReader + AV1Decoder 전체 플로우 검증
|
||||
- WebM 파일 열기 및 트랙 정보 출력
|
||||
- AV1 디코더 생성 및 초기화
|
||||
- 패킷 읽기 → 디코딩 → 통계 출력
|
||||
- 최대 5프레임 테스트 및 성능 측정
|
||||
|
||||
### 🚧 **다음 단계 구현 대기 중**
|
||||
1. **StreamingPipeline** - 멀티스레드 스트리밍 파이프라인
|
||||
2. **FileOutput** - Raw/BMP 파일 저장 기능
|
||||
3. **VP9Decoder** - VP9 지원 (미래 확장)
|
||||
4. **실제 WebM 파일 테스트** - 통합 테스트 실행
|
||||
|
||||
## 현재 상태
|
||||
- **진행률**: WebMFileReader ✅, AV1Decoder ✅, 통합테스트 ✅ (80%)
|
||||
- **빌드 상태**: ✅ 성공 (경고만 존재, 정상)
|
||||
- **다음 단계**: StreamingPipeline 구현 또는 FileOutput 구현
|
||||
- **확장성**: VP9 및 기타 코덱 지원 준비 완료
|
||||
|
||||
## 다음 구현 우선순위 제안
|
||||
1. **옵션 A**: StreamingPipeline 구현 (멀티스레드 파이프라인) - 30fps 실시간 재생
|
||||
2. **옵션 B**: FileOutput 구현 (Raw/BMP 파일 저장) - 디코딩 결과 검증
|
||||
3. **옵션 C**: 실제 WebM 파일 테스트 - 현재 구현 검증
|
||||
4. **옵션 D**: VP9Decoder 구현 - 추가 코덱 지원
|
||||
|
||||
### WebMFileReader 상세 구현 내역
|
||||
**파일**: `src/FileIO/WebMFileReader.h/.cpp`
|
||||
**기능**: libwebm 기반 WebM/MKV 파일 파싱 및 AV1 패킷 추출
|
||||
**주요 클래스**:
|
||||
- `WebMFileReader::MkvReader` - libwebm IMkvReader 구현
|
||||
- `WebMFileReader::InternalState` - 내부 상태 관리
|
||||
- `WebMUtils` - WebM 관련 유틸리티 함수들
|
||||
|
||||
**핵심 구현**:
|
||||
- 파일 I/O 및 libwebm 파서 연동
|
||||
- 비디오 트랙 열거 및 메타데이터 추출
|
||||
- 클러스터/블록 기반 패킷 순차 읽기
|
||||
- 시간/프레임 기반 탐색 알고리즘
|
||||
- 에러 처리 및 복구 메커니즘
|
||||
|
||||
### AV1Decoder 상세 구현 내역
|
||||
**파일**: `src/Decoder/AV1Decoder.h/.cpp`
|
||||
**기능**: dav1d 라이브러리 기반 AV1 비디오 디코딩
|
||||
**주요 구현**:
|
||||
- dav1d 컨텍스트 초기화 및 설정 관리
|
||||
- AV1 패킷 → Dav1dPicture → VideoFrame 변환 파이프라인
|
||||
- stride를 고려한 YUV 플레인 복사 최적화
|
||||
- 픽셀 포맷 자동 감지 (YUV420P/422P/444P)
|
||||
- 통계 수집 및 성능 측정
|
||||
|
||||
---
|
||||
*최종 업데이트: 2025-09-19 01:29*
|
||||
*Claude Code로 생성됨*
|
||||
43
vav2/Vav2Player/Vav2Player.sln
Normal file
@@ -0,0 +1,43 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36511.14 d17.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Vav2Player", "Vav2Player\Vav2Player.vcxproj", "{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|ARM64 = Debug|ARM64
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|ARM64 = Release|ARM64
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|ARM64.ActiveCfg = Debug|ARM64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|ARM64.Build.0 = Debug|ARM64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|ARM64.Deploy.0 = Debug|ARM64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|x64.Build.0 = Debug|x64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|x64.Deploy.0 = Debug|x64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|x86.ActiveCfg = Debug|Win32
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|x86.Build.0 = Debug|Win32
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Debug|x86.Deploy.0 = Debug|Win32
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|ARM64.ActiveCfg = Release|ARM64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|ARM64.Build.0 = Release|ARM64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|ARM64.Deploy.0 = Release|ARM64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|x64.ActiveCfg = Release|x64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|x64.Build.0 = Release|x64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|x64.Deploy.0 = Release|x64
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|x86.ActiveCfg = Release|Win32
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|x86.Build.0 = Release|Win32
|
||||
{C52EFC56-E19C-4568-9D83-A5A5E5282E1E}.Release|x86.Deploy.0 = Release|Win32
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {486351FC-86BE-4EE0-A88C-AC31CDFA028A}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
16
vav2/Vav2Player/Vav2Player/App.xaml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Application
|
||||
x:Class="Vav2Player.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Vav2Player">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
<!-- Other merged dictionaries here -->
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<!-- Other app resources here -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
281
vav2/Vav2Player/Vav2Player/App.xaml.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
#include "pch.h"
|
||||
#include "App.xaml.h"
|
||||
#include "MainWindow.xaml.h"
|
||||
#include "src/Console/HeadlessDecoder.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <signal.h>
|
||||
#include <crtdbg.h>
|
||||
|
||||
using namespace winrt;
|
||||
using namespace Microsoft::UI::Xaml;
|
||||
|
||||
// 헤드리스 모드용 에러 핸들러
|
||||
void HeadlessAbortHandler(int signal) {
|
||||
std::cerr << "\n*** FATAL ERROR ***" << std::endl;
|
||||
switch (signal) {
|
||||
case SIGABRT:
|
||||
std::cerr << "Program aborted (SIGABRT)" << std::endl;
|
||||
break;
|
||||
case SIGFPE:
|
||||
std::cerr << "Floating point exception (SIGFPE)" << std::endl;
|
||||
break;
|
||||
case SIGILL:
|
||||
std::cerr << "Illegal instruction (SIGILL)" << std::endl;
|
||||
break;
|
||||
case SIGINT:
|
||||
std::cerr << "Interrupt signal (SIGINT)" << std::endl;
|
||||
break;
|
||||
case SIGSEGV:
|
||||
std::cerr << "Segmentation violation (SIGSEGV)" << std::endl;
|
||||
break;
|
||||
case SIGTERM:
|
||||
std::cerr << "Termination request (SIGTERM)" << std::endl;
|
||||
break;
|
||||
default:
|
||||
std::cerr << "Unknown signal: " << signal << std::endl;
|
||||
break;
|
||||
}
|
||||
std::cerr << "Application will exit now." << std::endl;
|
||||
std::cerr.flush();
|
||||
|
||||
// 즉시 종료 (UI 팝업 방지)
|
||||
TerminateProcess(GetCurrentProcess(), 1);
|
||||
}
|
||||
|
||||
// abort() 후킹을 위한 사용자 정의 함수
|
||||
void custom_abort() {
|
||||
std::cerr << "\n*** CUSTOM ABORT CALLED ***" << std::endl;
|
||||
std::cerr << "Terminating process without UI..." << std::endl;
|
||||
std::cerr.flush();
|
||||
TerminateProcess(GetCurrentProcess(), 1);
|
||||
}
|
||||
|
||||
// 헤드리스 모드용 assertion 핸들러
|
||||
int HeadlessAssertHandler(int reportType, char* message, int* returnValue) {
|
||||
// 즉시 종료하여 어떤 UI도 표시하지 않음
|
||||
std::cerr << "\n*** CRT ERROR INTERCEPTED ***" << std::endl;
|
||||
std::cerr << "Type: ";
|
||||
switch (reportType) {
|
||||
case _CRT_WARN:
|
||||
std::cerr << "Warning";
|
||||
break;
|
||||
case _CRT_ERROR:
|
||||
std::cerr << "Error";
|
||||
break;
|
||||
case _CRT_ASSERT:
|
||||
std::cerr << "Assertion";
|
||||
break;
|
||||
default:
|
||||
std::cerr << "Unknown (" << reportType << ")";
|
||||
break;
|
||||
}
|
||||
std::cerr << std::endl;
|
||||
std::cerr << "Message: " << (message ? message : "Unknown") << std::endl;
|
||||
std::cerr << "Terminating immediately..." << std::endl;
|
||||
std::cerr.flush();
|
||||
|
||||
// 강제 즉시 종료 (UI 팝업 완전 방지)
|
||||
TerminateProcess(GetCurrentProcess(), 1);
|
||||
return TRUE; // 이 라인은 실행되지 않음
|
||||
}
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace winrt::Vav2Player::implementation
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </summary>
|
||||
App::App()
|
||||
{
|
||||
// Xaml objects should not call InitializeComponent during construction.
|
||||
// See https://github.com/microsoft/cppwinrt/tree/master/nuget#initializecomponent
|
||||
|
||||
#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
|
||||
UnhandledException([](IInspectable const&, UnhandledExceptionEventArgs const& e)
|
||||
{
|
||||
if (IsDebuggerPresent())
|
||||
{
|
||||
auto errorMessage = e.Message();
|
||||
__debugbreak();
|
||||
}
|
||||
});
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched.
|
||||
/// </summary>
|
||||
/// <param name="e">Details about the launch request and process.</param>
|
||||
void App::OnLaunched([[maybe_unused]] LaunchActivatedEventArgs const& e)
|
||||
{
|
||||
// 명령줄 인자 확인
|
||||
int argc = 0;
|
||||
LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
|
||||
// 인자가 있으면 헤드리스 모드로 실행
|
||||
if (argc >= 2) {
|
||||
// 헤드리스 모드용 에러 처리 설정
|
||||
// 1. 크래시 리포트 UI 비활성화 (더 강력한 설정)
|
||||
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX | SEM_NOALIGNMENTFAULTEXCEPT);
|
||||
|
||||
// 2. Windows Error Reporting 비활성화
|
||||
typedef BOOL(WINAPI* tGetPolicy)(LPDWORD lpFlags);
|
||||
typedef BOOL(WINAPI* tSetPolicy)(DWORD dwFlags);
|
||||
HMODULE hMod = LoadLibraryA("kernel32.dll");
|
||||
if (hMod) {
|
||||
tSetPolicy pSetPolicy = (tSetPolicy)GetProcAddress(hMod, "SetErrorMode");
|
||||
if (pSetPolicy) {
|
||||
pSetPolicy(SEM_NOGPFAULTERRORBOX);
|
||||
}
|
||||
FreeLibrary(hMod);
|
||||
}
|
||||
|
||||
// 3. CRT 에러 리포트 모드 설정 (Debug 빌드용 특별 처리)
|
||||
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG);
|
||||
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_DEBUG);
|
||||
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG);
|
||||
|
||||
// Debug 빌드에서 abort() UI 완전 비활성화
|
||||
#ifdef _DEBUG
|
||||
_CrtSetReportMode(_CRT_WARN, 0);
|
||||
_CrtSetReportMode(_CRT_ERROR, 0);
|
||||
_CrtSetReportMode(_CRT_ASSERT, 0);
|
||||
|
||||
// Debug CRT의 abort 동작 완전 비활성화
|
||||
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
|
||||
|
||||
// Debug heap의 assertion 비활성화
|
||||
int tmpFlag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG);
|
||||
tmpFlag &= ~_CRTDBG_ALLOC_MEM_DF;
|
||||
tmpFlag &= ~_CRTDBG_LEAK_CHECK_DF;
|
||||
_CrtSetDbgFlag(tmpFlag);
|
||||
#endif
|
||||
|
||||
// 4. 사용자 정의 에러 핸들러 설정 (모든 CRT 에러 가로채기)
|
||||
_CrtSetReportHook(HeadlessAssertHandler);
|
||||
|
||||
// 5. abort() 동작 설정 (Release 빌드용)
|
||||
_set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
|
||||
|
||||
// 6. 시그널 핸들러 설정 (즉시 종료)
|
||||
signal(SIGABRT, HeadlessAbortHandler);
|
||||
signal(SIGFPE, HeadlessAbortHandler);
|
||||
signal(SIGILL, HeadlessAbortHandler);
|
||||
signal(SIGINT, HeadlessAbortHandler);
|
||||
signal(SIGSEGV, HeadlessAbortHandler);
|
||||
signal(SIGTERM, HeadlessAbortHandler);
|
||||
|
||||
// 7. Windows 구조화된 예외 처리 설정
|
||||
SetUnhandledExceptionFilter([](EXCEPTION_POINTERS* ExceptionInfo) -> LONG {
|
||||
std::cerr << "\n*** UNHANDLED EXCEPTION ***" << std::endl;
|
||||
std::cerr << "Exception Code: 0x" << std::hex << ExceptionInfo->ExceptionRecord->ExceptionCode << std::endl;
|
||||
std::cerr << "Terminating process without UI..." << std::endl;
|
||||
std::cerr.flush();
|
||||
TerminateProcess(GetCurrentProcess(), 1);
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
});
|
||||
|
||||
// 8. CRT가 terminate를 호출할 때의 처리
|
||||
std::set_terminate([]() {
|
||||
std::cerr << "\n*** TERMINATE CALLED ***" << std::endl;
|
||||
std::cerr << "Terminating process without UI..." << std::endl;
|
||||
std::cerr.flush();
|
||||
TerminateProcess(GetCurrentProcess(), 1);
|
||||
});
|
||||
|
||||
// 콘솔 창 할당 (강제 설정)
|
||||
AllocConsole();
|
||||
|
||||
// 표준 입출력 스트림을 콘솔로 리디렉션
|
||||
FILE* pCout;
|
||||
FILE* pCerr;
|
||||
FILE* pCin;
|
||||
freopen_s(&pCout, "CONOUT$", "w", stdout);
|
||||
freopen_s(&pCerr, "CONOUT$", "w", stderr);
|
||||
freopen_s(&pCin, "CONIN$", "r", stdin);
|
||||
|
||||
// C++ 스트림도 동기화
|
||||
std::ios::sync_with_stdio(true);
|
||||
|
||||
// 즉시 테스트 출력
|
||||
std::cout << "Console initialized successfully!" << std::endl;
|
||||
std::cout.flush();
|
||||
std::cerr << "Error stream working!" << std::endl;
|
||||
std::cerr.flush();
|
||||
|
||||
// 디버그 로그 파일 생성
|
||||
std::ofstream debug_log("headless_debug.log");
|
||||
debug_log << "=== HEADLESS DEBUG LOG ===" << std::endl;
|
||||
debug_log << "Starting headless mode..." << std::endl;
|
||||
debug_log.flush();
|
||||
|
||||
// UTF-16 to UTF-8 변환 (더 안전한 방법)
|
||||
std::string input_file;
|
||||
int size_needed = WideCharToMultiByte(CP_UTF8, 0, argv[1], -1, NULL, 0, NULL, NULL);
|
||||
if (size_needed > 0) {
|
||||
input_file.resize(size_needed - 1);
|
||||
WideCharToMultiByte(CP_UTF8, 0, argv[1], -1, &input_file[0], size_needed, NULL, NULL);
|
||||
}
|
||||
|
||||
// 헤드리스 디코더 실행 (try-catch로 보호)
|
||||
bool success = false;
|
||||
try {
|
||||
debug_log << "Creating HeadlessDecoder..." << std::endl;
|
||||
debug_log.flush();
|
||||
std::cout << "Creating HeadlessDecoder..." << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
::Vav2Player::HeadlessDecoder decoder;
|
||||
debug_log << "HeadlessDecoder created successfully" << std::endl;
|
||||
debug_log.flush();
|
||||
std::cout << "HeadlessDecoder created successfully" << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
debug_log << "Calling ProcessFile with: " << input_file << std::endl;
|
||||
debug_log.flush();
|
||||
std::cout << "Calling ProcessFile..." << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
success = decoder.ProcessFile(input_file);
|
||||
debug_log << "ProcessFile completed with result: " << (success ? "SUCCESS" : "FAILURE") << std::endl;
|
||||
debug_log.flush();
|
||||
std::cout << "ProcessFile completed with result: " << (success ? "SUCCESS" : "FAILURE") << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
debug_log << "\n*** EXCEPTION in main: " << e.what() << std::endl;
|
||||
debug_log.flush();
|
||||
std::cerr << "\n*** EXCEPTION in main: " << e.what() << std::endl;
|
||||
success = false;
|
||||
}
|
||||
catch (...) {
|
||||
debug_log << "\n*** UNKNOWN EXCEPTION in main" << std::endl;
|
||||
debug_log.flush();
|
||||
std::cerr << "\n*** UNKNOWN EXCEPTION in main" << std::endl;
|
||||
success = false;
|
||||
}
|
||||
|
||||
debug_log << "Closing debug log..." << std::endl;
|
||||
debug_log.close();
|
||||
|
||||
// 결과에 따른 종료 코드
|
||||
int exit_code = success ? 0 : 1;
|
||||
|
||||
// 사용자 입력 대기 (디버그용)
|
||||
std::cout << "\nPress Enter to exit...";
|
||||
std::cin.get();
|
||||
|
||||
// 프로그램 종료
|
||||
LocalFree(argv);
|
||||
ExitProcess(exit_code);
|
||||
}
|
||||
|
||||
// 명령줄 인자가 없으면 일반 UI 모드
|
||||
LocalFree(argv);
|
||||
window = make<MainWindow>();
|
||||
window.Activate();
|
||||
}
|
||||
}
|
||||
16
vav2/Vav2Player/Vav2Player/App.xaml.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "App.xaml.g.h"
|
||||
|
||||
namespace winrt::Vav2Player::implementation
|
||||
{
|
||||
struct App : AppT<App>
|
||||
{
|
||||
App();
|
||||
|
||||
void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);
|
||||
|
||||
private:
|
||||
winrt::Microsoft::UI::Xaml::Window window{ nullptr };
|
||||
};
|
||||
}
|
||||
BIN
vav2/Vav2Player/Vav2Player/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
vav2/Vav2Player/Vav2Player/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
BIN
vav2/Vav2Player/Vav2Player/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
vav2/Vav2Player/Vav2Player/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
vav2/Vav2Player/Vav2Player/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
8
vav2/Vav2Player/Vav2Player/MainWindow.idl
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Vav2Player
|
||||
{
|
||||
[default_interface]
|
||||
runtimeclass MainWindow : Microsoft.UI.Xaml.Window
|
||||
{
|
||||
MainWindow();
|
||||
}
|
||||
}
|
||||
96
vav2/Vav2Player/Vav2Player/MainWindow.xaml
Normal file
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Window
|
||||
x:Class="Vav2Player.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Vav2Player"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Vav2Player">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Video Display Area -->
|
||||
<Border Grid.Row="0" Background="Black" BorderBrush="Gray" BorderThickness="1" Margin="10">
|
||||
<TextBlock x:Name="VideoDisplayArea"
|
||||
Text="AV1 Video will be displayed here"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="16"/>
|
||||
</Border>
|
||||
|
||||
<!-- Player Controls -->
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="10">
|
||||
<Button x:Name="OpenFileButton"
|
||||
Content="Open AV1 File"
|
||||
Width="120"
|
||||
Height="40"
|
||||
Margin="5"
|
||||
Click="OpenFileButton_Click"/>
|
||||
|
||||
<Button x:Name="TestDecodeButton"
|
||||
Content="Test Decode"
|
||||
Width="100"
|
||||
Height="40"
|
||||
Margin="5"
|
||||
Click="TestDecodeButton_Click"/>
|
||||
|
||||
<Button x:Name="PlayButton"
|
||||
Content="Play"
|
||||
Width="80"
|
||||
Height="40"
|
||||
Margin="5"
|
||||
IsEnabled="False"
|
||||
Click="PlayButton_Click"/>
|
||||
|
||||
<Button x:Name="PauseButton"
|
||||
Content="Pause"
|
||||
Width="80"
|
||||
Height="40"
|
||||
Margin="5"
|
||||
IsEnabled="False"
|
||||
Click="PauseButton_Click"/>
|
||||
|
||||
<Button x:Name="StopButton"
|
||||
Content="Stop"
|
||||
Width="80"
|
||||
Height="40"
|
||||
Margin="5"
|
||||
IsEnabled="False"
|
||||
Click="StopButton_Click"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Status and Info Panel -->
|
||||
<Border Grid.Row="2" Background="LightGray" Padding="10" Margin="10,0,10,10">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock x:Name="StatusText"
|
||||
Grid.Column="0"
|
||||
Text="Ready"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<TextBlock Text="Progress: " VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<ProgressBar x:Name="ProgressBar"
|
||||
Width="200"
|
||||
Height="10"
|
||||
VerticalAlignment="Center"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Value="0"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
556
vav2/Vav2Player/Vav2Player/MainWindow.xaml.cpp
Normal file
@@ -0,0 +1,556 @@
|
||||
#include "pch.h"
|
||||
#include "MainWindow.xaml.h"
|
||||
#if __has_include("MainWindow.g.cpp")
|
||||
#include "MainWindow.g.cpp"
|
||||
#endif
|
||||
|
||||
using namespace winrt;
|
||||
using namespace Microsoft::UI::Xaml;
|
||||
using namespace Microsoft::UI::Xaml::Controls;
|
||||
using namespace Windows::Foundation;
|
||||
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace winrt::Vav2Player::implementation
|
||||
{
|
||||
void MainWindow::OpenFileButton_Click(IInspectable const&, RoutedEventArgs const&)
|
||||
{
|
||||
try
|
||||
{
|
||||
OutputDebugStringA("OpenFileButton_Click called!\n");
|
||||
UpdateStatus("Opening file picker...");
|
||||
|
||||
// Call async file picker method
|
||||
OpenFileAsync();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error opening file");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::TestDecodeButton_Click(IInspectable const&, RoutedEventArgs const&)
|
||||
{
|
||||
try
|
||||
{
|
||||
OutputDebugStringA("TestDecodeButton_Click called!\n");
|
||||
UpdateStatus("Starting AV1 decode test...");
|
||||
|
||||
// Initialize components
|
||||
m_fileReader = std::make_unique<WebMFileReader>();
|
||||
m_decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1);
|
||||
m_fileOutput = std::make_unique<FileOutput>();
|
||||
|
||||
if (!m_decoder)
|
||||
{
|
||||
UpdateStatus("Failed to create AV1 decoder");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize decoder with basic metadata for testing
|
||||
VideoMetadata testMetadata;
|
||||
testMetadata.width = 1920;
|
||||
testMetadata.height = 1080;
|
||||
testMetadata.frame_rate = 30.0;
|
||||
testMetadata.codec_type = VideoCodecType::AV1;
|
||||
testMetadata.color_space = ColorSpace::YUV420P;
|
||||
|
||||
if (!m_decoder->Initialize(testMetadata))
|
||||
{
|
||||
UpdateStatus("Failed to initialize AV1 decoder");
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateStatus("AV1 decoder initialized successfully!");
|
||||
|
||||
// TODO: Add actual file processing logic
|
||||
ProcessSingleFrame();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error during decode test");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::PlayButton_Click(IInspectable const&, RoutedEventArgs const&)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!m_isFileLoaded)
|
||||
{
|
||||
UpdateStatus("No file loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize playback if not already done
|
||||
if (!m_playbackInitialized)
|
||||
{
|
||||
InitializePlayback();
|
||||
}
|
||||
|
||||
if (m_playbackInitialized)
|
||||
{
|
||||
m_isPlaying = true;
|
||||
UpdateButtons();
|
||||
UpdateStatus("Playing...");
|
||||
StartPlaybackTimer();
|
||||
OutputDebugStringA("Playback started\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateStatus("Error: Failed to initialize playback");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error starting playback");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::PauseButton_Click(IInspectable const&, RoutedEventArgs const&)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_isPlaying)
|
||||
{
|
||||
m_isPlaying = false;
|
||||
StopPlaybackTimer();
|
||||
UpdateButtons();
|
||||
UpdateStatus("Paused at frame " + std::to_string(m_currentFrame));
|
||||
OutputDebugStringA("Playback paused\n");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error pausing playback");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::StopButton_Click(IInspectable const&, RoutedEventArgs const&)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Stop playback
|
||||
m_isPlaying = false;
|
||||
StopPlaybackTimer();
|
||||
|
||||
// Reset to beginning
|
||||
m_currentFrame = 0;
|
||||
|
||||
// Reset file reader position
|
||||
if (m_fileReader && m_fileReader->IsFileOpen())
|
||||
{
|
||||
m_fileReader->Reset();
|
||||
}
|
||||
|
||||
// Reset decoder
|
||||
if (m_decoder && m_decoder->IsInitialized())
|
||||
{
|
||||
m_decoder->Reset();
|
||||
}
|
||||
|
||||
UpdateButtons();
|
||||
UpdateStatus("Stopped - Reset to beginning");
|
||||
|
||||
// Reset progress bar
|
||||
ProgressBar().Value(0);
|
||||
|
||||
OutputDebugStringA("Playback stopped and reset\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error stopping playback");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::UpdateStatus(const std::string& message)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Debug output to console
|
||||
OutputDebugStringA(("Status: " + message + "\n").c_str());
|
||||
|
||||
// Convert std::string to winrt::hstring
|
||||
auto hstr = winrt::to_hstring(message);
|
||||
StatusText().Text(hstr);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Fallback if status update fails
|
||||
OutputDebugStringA("Error updating status\n");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::UpdateButtons()
|
||||
{
|
||||
try
|
||||
{
|
||||
PlayButton().IsEnabled(m_isFileLoaded && !m_isPlaying);
|
||||
PauseButton().IsEnabled(m_isFileLoaded && m_isPlaying);
|
||||
StopButton().IsEnabled(m_isFileLoaded);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Fallback if button update fails
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::ProcessSingleFrame()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!m_fileReader || !m_decoder || !m_fileReader->IsFileOpen())
|
||||
{
|
||||
UpdateStatus("Error: File or decoder not ready");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read next packet from WebM file
|
||||
VideoPacket packet;
|
||||
if (!m_fileReader->ReadNextPacket(packet))
|
||||
{
|
||||
// End of file reached
|
||||
if (m_isPlaying)
|
||||
{
|
||||
m_isPlaying = false;
|
||||
StopPlaybackTimer();
|
||||
UpdateButtons();
|
||||
UpdateStatus("Playback completed - End of file reached");
|
||||
OutputDebugStringA("End of file reached\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Decode the packet
|
||||
VideoFrame frame;
|
||||
bool decodeSuccess = m_decoder->DecodeFrame(packet, frame);
|
||||
|
||||
if (decodeSuccess)
|
||||
{
|
||||
// Successfully decoded frame
|
||||
m_currentFrame++;
|
||||
|
||||
// Save frame to file if FileOutput is available
|
||||
if (m_fileOutput)
|
||||
{
|
||||
auto saveResult = m_fileOutput->SaveFrame(frame, m_currentFrame);
|
||||
if (saveResult.success)
|
||||
{
|
||||
OutputDebugStringA(("Saved frame " + std::to_string(m_currentFrame) +
|
||||
" to " + saveResult.saved_path.string() + "\n").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress
|
||||
UpdateProgress();
|
||||
|
||||
std::string statusMsg = "Frame " + std::to_string(m_currentFrame);
|
||||
if (m_totalFrames > 0)
|
||||
{
|
||||
statusMsg += "/" + std::to_string(m_totalFrames);
|
||||
}
|
||||
statusMsg += " decoded";
|
||||
|
||||
if (!m_isPlaying) // Only update status if not playing (to avoid spam)
|
||||
{
|
||||
UpdateStatus(statusMsg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Decoding failed - this might be normal for some packets
|
||||
OutputDebugStringA("Frame decoding failed (might be normal)\n");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error processing frame");
|
||||
}
|
||||
}
|
||||
|
||||
winrt::Windows::Foundation::IAsyncAction MainWindow::OpenFileAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create file picker
|
||||
winrt::Windows::Storage::Pickers::FileOpenPicker picker;
|
||||
|
||||
// Get the current window's HWND for WinUI3
|
||||
auto windowNative = this->try_as<::IWindowNative>();
|
||||
HWND hwnd = nullptr;
|
||||
if (windowNative)
|
||||
{
|
||||
windowNative->get_WindowHandle(&hwnd);
|
||||
}
|
||||
|
||||
// Initialize the picker with the window handle
|
||||
auto initializeWithWindow = picker.as<::IInitializeWithWindow>();
|
||||
if (hwnd)
|
||||
{
|
||||
initializeWithWindow->Initialize(hwnd);
|
||||
}
|
||||
|
||||
// Set file type filters for AV1/WebM/MKV files
|
||||
picker.ViewMode(winrt::Windows::Storage::Pickers::PickerViewMode::Thumbnail);
|
||||
picker.SuggestedStartLocation(winrt::Windows::Storage::Pickers::PickerLocationId::VideosLibrary);
|
||||
picker.FileTypeFilter().Append(L".webm");
|
||||
picker.FileTypeFilter().Append(L".mkv");
|
||||
picker.FileTypeFilter().Append(L".av1");
|
||||
picker.FileTypeFilter().Append(L".ivf");
|
||||
|
||||
// Open the file picker
|
||||
UpdateStatus("Please select an AV1/WebM video file...");
|
||||
auto file = co_await picker.PickSingleFileAsync();
|
||||
|
||||
if (file)
|
||||
{
|
||||
// Convert Windows::Storage::StorageFile path to std::string
|
||||
auto path = file.Path();
|
||||
std::string filePath = winrt::to_string(path);
|
||||
|
||||
OutputDebugStringA(("Selected file: " + filePath + "\n").c_str());
|
||||
std::string fileName = winrt::to_string(file.Name());
|
||||
UpdateStatus("File selected: " + fileName);
|
||||
|
||||
// Store the file path
|
||||
m_currentFilePath = filePath;
|
||||
|
||||
// Try to open the file with WebMFileReader
|
||||
if (!m_fileReader)
|
||||
{
|
||||
m_fileReader = std::make_unique<WebMFileReader>();
|
||||
}
|
||||
|
||||
OutputDebugStringA(("Attempting to open file: " + filePath + "\n").c_str());
|
||||
|
||||
// Check if file exists and get basic info
|
||||
std::ifstream file(filePath, std::ios::binary | std::ios::ate);
|
||||
if (file.is_open()) {
|
||||
auto fileSize = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Read first 16 bytes to check magic numbers
|
||||
char buffer[16];
|
||||
file.read(buffer, 16);
|
||||
file.close();
|
||||
|
||||
std::string hexString;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
char hex[4];
|
||||
sprintf_s(hex, "%02X ", (unsigned char)buffer[i]);
|
||||
hexString += hex;
|
||||
}
|
||||
|
||||
OutputDebugStringA(("File size: " + std::to_string(fileSize) + " bytes\n").c_str());
|
||||
OutputDebugStringA(("First 16 bytes: " + hexString + "\n").c_str());
|
||||
} else {
|
||||
OutputDebugStringA("Cannot open file for reading\n");
|
||||
}
|
||||
|
||||
if (m_fileReader->OpenFile(filePath))
|
||||
{
|
||||
OutputDebugStringA("WebMFileReader::OpenFile succeeded\n");
|
||||
// Get video metadata
|
||||
auto tracks = m_fileReader->GetVideoTracks();
|
||||
if (!tracks.empty())
|
||||
{
|
||||
// Select the first video track
|
||||
if (m_fileReader->SelectVideoTrack(tracks[0].track_number))
|
||||
{
|
||||
auto metadata = m_fileReader->GetVideoMetadata();
|
||||
|
||||
std::string info = "File opened successfully!\n";
|
||||
info += "Resolution: " + std::to_string(metadata.width) + "x" + std::to_string(metadata.height) + "\n";
|
||||
info += "Codec: " + metadata.codec_name + "\n";
|
||||
info += "Frames: " + std::to_string(metadata.total_frames);
|
||||
|
||||
UpdateStatus(info);
|
||||
m_isFileLoaded = true;
|
||||
|
||||
// Reset playback state for new file
|
||||
m_playbackInitialized = false;
|
||||
m_currentFrame = 0;
|
||||
m_totalFrames = metadata.total_frames;
|
||||
m_frameRate = metadata.frame_rate > 0 ? metadata.frame_rate : 30.0;
|
||||
|
||||
UpdateButtons();
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string errorMessage = m_fileReader->GetLastErrorString();
|
||||
UpdateStatus("Error: Could not select video track - " + errorMessage);
|
||||
OutputDebugStringA(("Track selection error: " + errorMessage + "\n").c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string errorMessage = m_fileReader->GetLastErrorString();
|
||||
UpdateStatus("Error: No video tracks found in file - " + errorMessage);
|
||||
OutputDebugStringA(("No video tracks error: " + errorMessage + "\n").c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get detailed error information from WebMFileReader
|
||||
auto errorCode = m_fileReader->GetLastError();
|
||||
std::string errorMessage = m_fileReader->GetLastErrorString();
|
||||
|
||||
OutputDebugStringA("WebMFileReader::OpenFile failed\n");
|
||||
OutputDebugStringA(("Error details: " + errorMessage + "\n").c_str());
|
||||
|
||||
std::string statusMessage = "Error: " + errorMessage;
|
||||
UpdateStatus(statusMessage);
|
||||
|
||||
// Additional debugging info
|
||||
std::string debugInfo = "Failed to open: " + filePath + " (Error: " + errorMessage + ")";
|
||||
OutputDebugStringA((debugInfo + "\n").c_str());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateStatus("File selection cancelled");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error opening file picker");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::UpdateProgress()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_totalFrames > 0)
|
||||
{
|
||||
double progress = (double)m_currentFrame / (double)m_totalFrames * 100.0;
|
||||
ProgressBar().Value(progress);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Error updating progress
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::InitializePlayback()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!m_fileReader || !m_fileReader->IsFileOpen())
|
||||
{
|
||||
UpdateStatus("Error: No file loaded");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get video metadata
|
||||
auto metadata = m_fileReader->GetVideoMetadata();
|
||||
m_totalFrames = metadata.total_frames;
|
||||
m_frameRate = metadata.frame_rate > 0 ? metadata.frame_rate : 30.0;
|
||||
|
||||
// Initialize decoder if not already done
|
||||
if (!m_decoder)
|
||||
{
|
||||
m_decoder = VideoDecoderFactory::CreateDecoder(VideoCodecType::AV1);
|
||||
}
|
||||
|
||||
if (m_decoder && !m_decoder->IsInitialized())
|
||||
{
|
||||
if (!m_decoder->Initialize(metadata))
|
||||
{
|
||||
UpdateStatus("Error: Failed to initialize decoder");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize FileOutput if not already done
|
||||
if (!m_fileOutput)
|
||||
{
|
||||
m_fileOutput = std::make_unique<FileOutput>();
|
||||
|
||||
// Configure output to save frames as BMP files
|
||||
FileOutput::OutputConfig config;
|
||||
config.format = FileOutput::OutputFormat::BMP;
|
||||
config.output_directory = "output_frames";
|
||||
config.filename_prefix = "frame";
|
||||
config.create_subdirectories = true;
|
||||
config.overwrite_existing = true;
|
||||
|
||||
m_fileOutput->SetConfig(config);
|
||||
m_fileOutput->CreateOutputDirectory();
|
||||
}
|
||||
|
||||
// Reset playback position
|
||||
m_currentFrame = 0;
|
||||
m_fileReader->Reset();
|
||||
if (m_decoder)
|
||||
{
|
||||
m_decoder->Reset();
|
||||
}
|
||||
|
||||
m_playbackInitialized = true;
|
||||
UpdateStatus("Playback initialized - Ready to play");
|
||||
OutputDebugStringA("Playback initialized successfully\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error initializing playback");
|
||||
m_playbackInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::StartPlaybackTimer()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_playbackTimer)
|
||||
{
|
||||
m_playbackTimer.Stop();
|
||||
}
|
||||
|
||||
// Calculate timer interval based on frame rate
|
||||
auto intervalMs = std::chrono::milliseconds(static_cast<int>(1000.0 / m_frameRate));
|
||||
|
||||
// Create timer
|
||||
auto dispatcherQueue = winrt::Microsoft::UI::Dispatching::DispatcherQueue::GetForCurrentThread();
|
||||
m_playbackTimer = dispatcherQueue.CreateTimer();
|
||||
|
||||
// Set timer callback
|
||||
m_playbackTimer.Tick([this](auto&&, auto&&)
|
||||
{
|
||||
if (m_isPlaying)
|
||||
{
|
||||
ProcessSingleFrame();
|
||||
}
|
||||
});
|
||||
|
||||
// Set interval and start
|
||||
m_playbackTimer.Interval(intervalMs);
|
||||
m_playbackTimer.Start();
|
||||
|
||||
OutputDebugStringA(("Playback timer started with interval: " +
|
||||
std::to_string(intervalMs.count()) + "ms\n").c_str());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
UpdateStatus("Error starting playback timer");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::StopPlaybackTimer()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (m_playbackTimer)
|
||||
{
|
||||
m_playbackTimer.Stop();
|
||||
OutputDebugStringA("Playback timer stopped\n");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
// Error stopping timer
|
||||
}
|
||||
}
|
||||
}
|
||||
68
vav2/Vav2Player/Vav2Player/MainWindow.xaml.h
Normal file
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "MainWindow.g.h"
|
||||
#include "src/FileIO/WebMFileReader.h"
|
||||
#include "src/Decoder/VideoDecoderFactory.h"
|
||||
#include "src/Output/FileOutput.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace winrt::Vav2Player::implementation
|
||||
{
|
||||
struct MainWindow : MainWindowT<MainWindow>
|
||||
{
|
||||
MainWindow()
|
||||
{
|
||||
// Initialize XAML components and event handlers
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
void OpenFileButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
|
||||
void TestDecodeButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
|
||||
void PlayButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
|
||||
void PauseButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
|
||||
void StopButton_Click(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);
|
||||
|
||||
private:
|
||||
// Video processing components
|
||||
std::unique_ptr<WebMFileReader> m_fileReader;
|
||||
std::unique_ptr<IVideoDecoder> m_decoder;
|
||||
std::unique_ptr<FileOutput> m_fileOutput;
|
||||
|
||||
// Current file path
|
||||
std::string m_currentFilePath;
|
||||
|
||||
// UI state
|
||||
bool m_isPlaying = false;
|
||||
bool m_isFileLoaded = false;
|
||||
|
||||
// Playback state
|
||||
uint64_t m_currentFrame = 0;
|
||||
uint64_t m_totalFrames = 0;
|
||||
double m_frameRate = 30.0;
|
||||
bool m_playbackInitialized = false;
|
||||
|
||||
// Timer for playback
|
||||
winrt::Microsoft::UI::Dispatching::DispatcherQueueTimer m_playbackTimer{ nullptr };
|
||||
|
||||
// Helper methods
|
||||
void UpdateStatus(const std::string& message);
|
||||
void UpdateButtons();
|
||||
void ProcessSingleFrame();
|
||||
void UpdateProgress();
|
||||
void InitializePlayback();
|
||||
void StartPlaybackTimer();
|
||||
void StopPlaybackTimer();
|
||||
|
||||
// File picker helper
|
||||
winrt::Windows::Foundation::IAsyncAction OpenFileAsync();
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Vav2Player::factory_implementation
|
||||
{
|
||||
struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
|
||||
{
|
||||
};
|
||||
}
|
||||
51
vav2/Vav2Player/Vav2Player/Package.appxmanifest
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<Package
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap rescap">
|
||||
|
||||
<Identity
|
||||
Name="1d1f832a-fe67-4156-8e31-04f03500503c"
|
||||
Publisher="CN=ened"
|
||||
Version="1.0.0.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="1d1f832a-fe67-4156-8e31-04f03500503c" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Vav2Player</DisplayName>
|
||||
<PublisherDisplayName>ened</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||
</Dependencies>
|
||||
|
||||
<Resources>
|
||||
<Resource Language="x-generate"/>
|
||||
</Resources>
|
||||
|
||||
<Applications>
|
||||
<Application Id="App"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$">
|
||||
<uap:VisualElements
|
||||
DisplayName="Vav2Player"
|
||||
Description="Vav2Player"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust" />
|
||||
</Capabilities>
|
||||
</Package>
|
||||
257
vav2/Vav2Player/Vav2Player/Vav2Player.vcxproj
Normal file
@@ -0,0 +1,257 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.props" Condition="Exists('..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.props')" />
|
||||
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" />
|
||||
<Import Project="..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props" Condition="Exists('..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<CppWinRTOptimized>true</CppWinRTOptimized>
|
||||
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
|
||||
<MinimalCoreWin>true</MinimalCoreWin>
|
||||
<ProjectGuid>{c52efc56-e19c-4568-9d83-a5a5e5282e1e}</ProjectGuid>
|
||||
<ProjectName>Vav2Player</ProjectName>
|
||||
<RootNamespace>Vav2Player</RootNamespace>
|
||||
<!--
|
||||
$(TargetName) should be same as $(RootNamespace) so that the produced binaries (.exe/.pri/etc.)
|
||||
have a name that matches the .winmd
|
||||
-->
|
||||
<TargetName>$(RootNamespace)</TargetName>
|
||||
<DefaultLanguage>ko-KR</DefaultLanguage>
|
||||
<MinimumVisualStudioVersion>16.0</MinimumVisualStudioVersion>
|
||||
<AppContainerApplication>false</AppContainerApplication>
|
||||
<AppxPackage>true</AppxPackage>
|
||||
<ApplicationType>Windows Store</ApplicationType>
|
||||
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
<WindowsTargetPlatformMinVersion>10.0.17763.0</WindowsTargetPlatformMinVersion>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|ARM64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|ARM64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>ARM64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<PlatformToolset>v143</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
<DesktopCompatible>true</DesktopCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<LinkIncremental>true</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<LinkIncremental>false</LinkIncremental>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PrecompiledHeader>Use</PrecompiledHeader>
|
||||
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
|
||||
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
|
||||
<WarningLevel>Level4</WarningLevel>
|
||||
<AdditionalOptions>%(AdditionalOptions) /bigobj</AdditionalOptions>
|
||||
<AdditionalIncludeDirectories>$(ProjectDir)..\..\..\include\libwebm;$(ProjectDir)..\..\..\include\dav1d;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<!-- Common link settings only - specific dependencies are in Debug/Release configurations -->
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(ProjectDir)..\..\..\lib\libwebm;$(ProjectDir)..\..\..\lib\dav1d;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>webm-debug.lib;dav1d-debug.lib</AdditionalDependencies>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<AdditionalLibraryDirectories>$(ProjectDir)..\..\..\lib\libwebm;$(ProjectDir)..\..\..\lib\dav1d;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
|
||||
<AdditionalDependencies>webm.lib;dav1d.lib</AdditionalDependencies>
|
||||
<EnableCOMDATFolding>true</EnableCOMDATFolding>
|
||||
<OptimizeReferences>true</OptimizeReferences>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup Condition="'$(WindowsPackageType)'!='None' and Exists('Package.appxmanifest')">
|
||||
<AppxManifest Include="Package.appxmanifest">
|
||||
<SubType>Designer</SubType>
|
||||
</AppxManifest>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="App.xaml.h">
|
||||
<DependentUpon>App.xaml</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="MainWindow.xaml.h">
|
||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="src\Common\VideoTypes.h" />
|
||||
<ClInclude Include="src\Decoder\IVideoDecoder.h" />
|
||||
<ClInclude Include="src\Decoder\VideoDecoderFactory.h" />
|
||||
<ClInclude Include="src\Decoder\AV1Decoder.h" />
|
||||
<ClInclude Include="src\FileIO\WebMFileReader.h" />
|
||||
<ClInclude Include="src\Pipeline\FrameBuffer.h" />
|
||||
<ClInclude Include="src\Pipeline\StreamingPipeline.h" />
|
||||
<ClInclude Include="src\Output\FileOutput.h" />
|
||||
<ClInclude Include="src\TestMain.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="App.xaml" />
|
||||
<Page Include="MainWindow.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp">
|
||||
<PrecompiledHeader>Create</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<ClCompile Include="App.xaml.cpp">
|
||||
<DependentUpon>App.xaml</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="MainWindow.xaml.cpp">
|
||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="src\Decoder\VideoDecoderFactory.cpp" />
|
||||
<ClCompile Include="src\Decoder\AV1Decoder.cpp" />
|
||||
<ClCompile Include="src\FileIO\WebMFileReader.cpp" />
|
||||
<ClCompile Include="src\Pipeline\FrameBuffer.cpp" />
|
||||
<ClCompile Include="src\Pipeline\StreamingPipeline.cpp" />
|
||||
<ClCompile Include="src\Output\FileOutput.cpp" />
|
||||
<ClCompile Include="src\Console\HeadlessDecoder.cpp" />
|
||||
<ClCompile Include="src\TestMain.cpp" />
|
||||
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="MainWindow.idl">
|
||||
<SubType>Code</SubType>
|
||||
<DependentUpon>MainWindow.xaml</DependentUpon>
|
||||
</Midl>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Text Include="readme.txt">
|
||||
<DeploymentContent>false</DeploymentContent>
|
||||
</Text>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="Assets\LockScreenLogo.scale-200.png" />
|
||||
<Image Include="Assets\SplashScreen.scale-200.png" />
|
||||
<Image Include="Assets\Square150x150Logo.scale-200.png" />
|
||||
<Image Include="Assets\Square44x44Logo.scale-200.png" />
|
||||
<Image Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||
<Image Include="Assets\StoreLogo.png" />
|
||||
<Image Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
<!--
|
||||
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||
package has not yet been restored.
|
||||
-->
|
||||
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<!--
|
||||
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||
the Windows App SDK Nuget package has not yet been restored.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
<Import Project="..\packages\Microsoft.Web.WebView2.1.0.3179.45\build\native\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.3179.45\build\native\Microsoft.Web.WebView2.targets')" />
|
||||
<Import Project="..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets" Condition="Exists('..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" />
|
||||
<Import Project="..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets" Condition="Exists('..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" />
|
||||
<Import Project="..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" />
|
||||
<Import Project="..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.targets" Condition="Exists('..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.targets')" />
|
||||
<Import Project="..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets" Condition="Exists('..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" />
|
||||
</ImportGroup>
|
||||
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
|
||||
<PropertyGroup>
|
||||
<ErrorText>이 프로젝트는 이 컴퓨터에 없는 NuGet 패키지를 참조합니다. 해당 패키지를 다운로드하려면 NuGet 패키지 복원을 사용하십시오. 자세한 내용은 http://go.microsoft.com/fwlink/?LinkID=322105를 참조하십시오. 누락된 파일은 {0}입니다.</ErrorText>
|
||||
</PropertyGroup>
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Web.WebView2.1.0.3179.45\build\native\Microsoft.Web.WebView2.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Web.WebView2.1.0.3179.45\build\native\Microsoft.Web.WebView2.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.SDK.BuildTools.MSIX.1.7.20250829.1\build\Microsoft.Windows.SDK.BuildTools.MSIX.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Base.1.8.250831001\build\native\Microsoft.WindowsAppSDK.Base.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.DWrite.1.8.25090401\build\Microsoft.WindowsAppSDK.DWrite.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.InteractiveExperiences.1.8.250906004\build\native\Microsoft.WindowsAppSDK.InteractiveExperiences.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Foundation.1.8.250906002\build\native\Microsoft.WindowsAppSDK.Foundation.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.AI.1.8.37\build\native\Microsoft.WindowsAppSDK.AI.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Runtime.1.8.250907003\build\native\Microsoft.WindowsAppSDK.Runtime.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.Widgets.1.8.250904007\build\native\Microsoft.WindowsAppSDK.Widgets.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.WinUI.1.8.250906003\build\native\Microsoft.WindowsAppSDK.WinUI.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.WindowsAppSDK.1.8.250907003\build\native\Microsoft.WindowsAppSDK.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.CppWinRT.2.0.250303.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.props'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.SDK.BuildTools.10.0.26100.4948\build\Microsoft.Windows.SDK.BuildTools.targets'))" />
|
||||
<Error Condition="!Exists('..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\Microsoft.Windows.ImplementationLibrary.1.0.250325.1\build\native\Microsoft.Windows.ImplementationLibrary.targets'))" />
|
||||
</Target>
|
||||
</Project>
|
||||
59
vav2/Vav2Player/Vav2Player/Vav2Player.vcxproj.filters
Normal file
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Include="MainWindow.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="MainWindow.idl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Image Include="Assets\Wide310x150Logo.scale-200.png">
|
||||
<Filter>Assets</Filter>
|
||||
</Image>
|
||||
<Image Include="Assets\StoreLogo.png">
|
||||
<Filter>Assets</Filter>
|
||||
</Image>
|
||||
<Image Include="Assets\Square150x150Logo.scale-200.png">
|
||||
<Filter>Assets</Filter>
|
||||
</Image>
|
||||
<Image Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png">
|
||||
<Filter>Assets</Filter>
|
||||
</Image>
|
||||
<Image Include="Assets\Square44x44Logo.scale-200.png">
|
||||
<Filter>Assets</Filter>
|
||||
</Image>
|
||||
<Image Include="Assets\SplashScreen.scale-200.png">
|
||||
<Filter>Assets</Filter>
|
||||
</Image>
|
||||
<Image Include="Assets\LockScreenLogo.scale-200.png">
|
||||
<Filter>Assets</Filter>
|
||||
</Image>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Filter Include="Assets">
|
||||
<UniqueIdentifier>{c52efc56-e19c-4568-9d83-a5a5e5282e1e}</UniqueIdentifier>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Text Include="readme.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Manifest Include="app.manifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AppxManifest Include="Package.appxmanifest" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
4
vav2/Vav2Player/Vav2Player/Vav2Player.vcxproj.user
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup />
|
||||
</Project>
|
||||
19
vav2/Vav2Player/Vav2Player/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Vav2Player.app"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
17
vav2/Vav2Player/Vav2Player/packages.config
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.3179.45" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.CppWinRT" version="2.0.250303.1" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.250325.1" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools" version="10.0.26100.4948" targetFramework="native" />
|
||||
<package id="Microsoft.Windows.SDK.BuildTools.MSIX" version="1.7.20250829.1" targetFramework="native" developmentDependency="true" />
|
||||
<package id="Microsoft.WindowsAppSDK" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.AI" version="1.8.37" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Base" version="1.8.250831001" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.DWrite" version="1.8.25090401" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Foundation" version="1.8.250906002" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.InteractiveExperiences" version="1.8.250906004" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Runtime" version="1.8.250907003" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.Widgets" version="1.8.250904007" targetFramework="native" />
|
||||
<package id="Microsoft.WindowsAppSDK.WinUI" version="1.8.250906003" targetFramework="native" />
|
||||
</packages>
|
||||
1
vav2/Vav2Player/Vav2Player/pch.cpp
Normal file
@@ -0,0 +1 @@
|
||||
#include "pch.h"
|
||||
50
vav2/Vav2Player/Vav2Player/pch.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include <windows.h>
|
||||
#include <unknwn.h>
|
||||
#include <restrictederrorinfo.h>
|
||||
#include <hstring.h>
|
||||
|
||||
// Undefine GetCurrentTime macro to prevent
|
||||
// conflict with Storyboard::GetCurrentTime
|
||||
#undef GetCurrentTime
|
||||
|
||||
#include <winrt/Windows.Foundation.h>
|
||||
#include <winrt/Windows.Foundation.Collections.h>
|
||||
#include <winrt/Windows.ApplicationModel.Activation.h>
|
||||
#include <winrt/Microsoft.UI.Composition.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Controls.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Controls.Primitives.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Data.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Interop.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Markup.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Media.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Navigation.h>
|
||||
#include <winrt/Microsoft.UI.Xaml.Shapes.h>
|
||||
#include <winrt/Microsoft.UI.Dispatching.h>
|
||||
#include <wil/cppwinrt_helpers.h>
|
||||
|
||||
// File picker support
|
||||
#include <winrt/Windows.Storage.h>
|
||||
#include <winrt/Windows.Storage.Pickers.h>
|
||||
#include <winrt/Windows.Storage.Streams.h>
|
||||
#include <shobjidl.h> // For IInitializeWithWindow
|
||||
#include <microsoft.ui.xaml.window.h> // For IWindowNative
|
||||
|
||||
// AV1 Player libraries
|
||||
#include <mkvparser.hpp>
|
||||
#include <dav1d.h>
|
||||
|
||||
// Video processing components
|
||||
#include "src/Common/VideoTypes.h"
|
||||
#include "src/Decoder/IVideoDecoder.h"
|
||||
#include "src/Decoder/VideoDecoderFactory.h"
|
||||
#include "src/FileIO/WebMFileReader.h"
|
||||
#include "src/Output/FileOutput.h"
|
||||
|
||||
// Standard library headers
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
|
||||
// Using namespace for video components
|
||||
using namespace Vav2Player;
|
||||
27
vav2/Vav2Player/Vav2Player/readme.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
========================================================================
|
||||
Vav2Player Project Overview
|
||||
========================================================================
|
||||
|
||||
This project demonstrates how to get started writing WinUI3 apps directly
|
||||
with standard C++, using the Windows App SDK and C++/WinRT packages and
|
||||
XAML compiler support to generate implementation headers from interface
|
||||
(IDL) files. These headers can then be used to implement the local
|
||||
Windows Runtime classes referenced in the app's XAML pages.
|
||||
|
||||
Steps:
|
||||
1. Create an interface (IDL) file to define any local Windows Runtime
|
||||
classes referenced in the app's XAML pages.
|
||||
2. Build the project once to generate implementation templates under
|
||||
the "Generated Files" folder, as well as skeleton class definitions
|
||||
under "Generated Files\sources".
|
||||
3. Use the skeleton class definitions for reference to implement your
|
||||
Windows Runtime classes.
|
||||
|
||||
========================================================================
|
||||
Learn more about Windows App SDK here:
|
||||
https://docs.microsoft.com/windows/apps/windows-app-sdk/
|
||||
Learn more about WinUI3 here:
|
||||
https://docs.microsoft.com/windows/apps/winui/winui3/
|
||||
Learn more about C++/WinRT here:
|
||||
http://aka.ms/cppwinrt/
|
||||
========================================================================
|
||||
244
vav2/Vav2Player/Vav2Player/src/Common/VideoTypes.h
Normal file
@@ -0,0 +1,244 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 비디오 코덱 타입
|
||||
enum class VideoCodecType {
|
||||
AV1,
|
||||
VP9,
|
||||
VP8, // TODO: 미래 확장
|
||||
H264, // TODO: 미래 확장
|
||||
H265 // TODO: 미래 확장
|
||||
};
|
||||
|
||||
// 색상 공간 정의
|
||||
enum class ColorSpace {
|
||||
YUV420P,
|
||||
YUV422P,
|
||||
YUV444P,
|
||||
RGB24,
|
||||
RGB32
|
||||
};
|
||||
|
||||
// 비디오 메타데이터
|
||||
struct VideoMetadata {
|
||||
// 기본 비디오 정보
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
double frame_rate = 0.0;
|
||||
uint64_t total_frames = 0;
|
||||
double duration_seconds = 0.0;
|
||||
|
||||
// 코덱 정보
|
||||
VideoCodecType codec_type = VideoCodecType::AV1;
|
||||
std::string codec_name;
|
||||
ColorSpace color_space = ColorSpace::YUV420P;
|
||||
|
||||
// 픽셀 포맷 정보
|
||||
uint32_t bit_depth = 8;
|
||||
uint32_t chroma_subsampling_x = 1;
|
||||
uint32_t chroma_subsampling_y = 1;
|
||||
|
||||
// 파일 정보
|
||||
std::string file_path;
|
||||
uint64_t file_size = 0;
|
||||
|
||||
bool IsValid() const {
|
||||
return width > 0 && height > 0 && frame_rate > 0.0;
|
||||
}
|
||||
};
|
||||
|
||||
// 디코딩된 비디오 프레임
|
||||
struct VideoFrame {
|
||||
// 프레임 메타데이터
|
||||
uint64_t frame_index = 0;
|
||||
double timestamp_seconds = 0.0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
ColorSpace color_space = ColorSpace::YUV420P;
|
||||
|
||||
// YUV 데이터 (각 플레인별)
|
||||
std::unique_ptr<uint8_t[]> y_plane;
|
||||
std::unique_ptr<uint8_t[]> u_plane;
|
||||
std::unique_ptr<uint8_t[]> v_plane;
|
||||
|
||||
// 각 플레인의 stride (실제 메모리 폭)
|
||||
uint32_t y_stride = 0;
|
||||
uint32_t u_stride = 0;
|
||||
uint32_t v_stride = 0;
|
||||
|
||||
// 각 플레인의 크기
|
||||
uint32_t y_size = 0;
|
||||
uint32_t u_size = 0;
|
||||
uint32_t v_size = 0;
|
||||
|
||||
// 프레임 상태
|
||||
bool is_keyframe = false;
|
||||
bool is_valid = false;
|
||||
|
||||
// 생성자
|
||||
VideoFrame() = default;
|
||||
|
||||
// 복사 방지 (대신 이동 시맨틱 사용)
|
||||
VideoFrame(const VideoFrame&) = delete;
|
||||
VideoFrame& operator=(const VideoFrame&) = delete;
|
||||
|
||||
// 이동 시맨틱
|
||||
VideoFrame(VideoFrame&& other) noexcept
|
||||
: frame_index(other.frame_index)
|
||||
, timestamp_seconds(other.timestamp_seconds)
|
||||
, width(other.width)
|
||||
, height(other.height)
|
||||
, color_space(other.color_space)
|
||||
, y_plane(std::move(other.y_plane))
|
||||
, u_plane(std::move(other.u_plane))
|
||||
, v_plane(std::move(other.v_plane))
|
||||
, y_stride(other.y_stride)
|
||||
, u_stride(other.u_stride)
|
||||
, v_stride(other.v_stride)
|
||||
, y_size(other.y_size)
|
||||
, u_size(other.u_size)
|
||||
, v_size(other.v_size)
|
||||
, is_keyframe(other.is_keyframe)
|
||||
, is_valid(other.is_valid) {
|
||||
// 원본 객체 초기화
|
||||
other.Reset();
|
||||
}
|
||||
|
||||
VideoFrame& operator=(VideoFrame&& other) noexcept {
|
||||
if (this != &other) {
|
||||
// 기존 데이터 해제는 unique_ptr이 자동으로 처리
|
||||
frame_index = other.frame_index;
|
||||
timestamp_seconds = other.timestamp_seconds;
|
||||
width = other.width;
|
||||
height = other.height;
|
||||
color_space = other.color_space;
|
||||
y_plane = std::move(other.y_plane);
|
||||
u_plane = std::move(other.u_plane);
|
||||
v_plane = std::move(other.v_plane);
|
||||
y_stride = other.y_stride;
|
||||
u_stride = other.u_stride;
|
||||
v_stride = other.v_stride;
|
||||
y_size = other.y_size;
|
||||
u_size = other.u_size;
|
||||
v_size = other.v_size;
|
||||
is_keyframe = other.is_keyframe;
|
||||
is_valid = other.is_valid;
|
||||
|
||||
other.Reset();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 프레임 메모리 할당 (YUV420P 기준)
|
||||
bool AllocateYUV420P(uint32_t frame_width, uint32_t frame_height) {
|
||||
width = frame_width;
|
||||
height = frame_height;
|
||||
color_space = ColorSpace::YUV420P;
|
||||
|
||||
// YUV420P: Y는 원본 크기, U/V는 1/4 크기
|
||||
y_stride = width;
|
||||
u_stride = width / 2;
|
||||
v_stride = width / 2;
|
||||
|
||||
y_size = y_stride * height;
|
||||
u_size = u_stride * (height / 2);
|
||||
v_size = v_stride * (height / 2);
|
||||
|
||||
try {
|
||||
y_plane = std::make_unique<uint8_t[]>(y_size);
|
||||
u_plane = std::make_unique<uint8_t[]>(u_size);
|
||||
v_plane = std::make_unique<uint8_t[]>(v_size);
|
||||
is_valid = true;
|
||||
return true;
|
||||
} catch (...) {
|
||||
Reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 프레임 초기화
|
||||
void Reset() {
|
||||
frame_index = 0;
|
||||
timestamp_seconds = 0.0;
|
||||
width = 0;
|
||||
height = 0;
|
||||
color_space = ColorSpace::YUV420P;
|
||||
y_plane.reset();
|
||||
u_plane.reset();
|
||||
v_plane.reset();
|
||||
y_stride = u_stride = v_stride = 0;
|
||||
y_size = u_size = v_size = 0;
|
||||
is_keyframe = false;
|
||||
is_valid = false;
|
||||
}
|
||||
|
||||
// 전체 프레임 크기 반환
|
||||
uint32_t GetTotalSize() const {
|
||||
return y_size + u_size + v_size;
|
||||
}
|
||||
};
|
||||
|
||||
// 비디오 패킷 (압축된 데이터)
|
||||
struct VideoPacket {
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t size = 0;
|
||||
uint64_t frame_index = 0;
|
||||
double timestamp_seconds = 0.0;
|
||||
bool is_keyframe = false;
|
||||
|
||||
VideoPacket() = default;
|
||||
|
||||
// 복사 방지, 이동만 허용
|
||||
VideoPacket(const VideoPacket&) = delete;
|
||||
VideoPacket& operator=(const VideoPacket&) = delete;
|
||||
|
||||
VideoPacket(VideoPacket&& other) noexcept
|
||||
: data(std::move(other.data))
|
||||
, size(other.size)
|
||||
, frame_index(other.frame_index)
|
||||
, timestamp_seconds(other.timestamp_seconds)
|
||||
, is_keyframe(other.is_keyframe) {
|
||||
other.size = 0;
|
||||
other.frame_index = 0;
|
||||
other.timestamp_seconds = 0.0;
|
||||
other.is_keyframe = false;
|
||||
}
|
||||
|
||||
VideoPacket& operator=(VideoPacket&& other) noexcept {
|
||||
if (this != &other) {
|
||||
data = std::move(other.data);
|
||||
size = other.size;
|
||||
frame_index = other.frame_index;
|
||||
timestamp_seconds = other.timestamp_seconds;
|
||||
is_keyframe = other.is_keyframe;
|
||||
|
||||
other.size = 0;
|
||||
other.frame_index = 0;
|
||||
other.timestamp_seconds = 0.0;
|
||||
other.is_keyframe = false;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool AllocateData(size_t packet_size) {
|
||||
try {
|
||||
data = std::make_unique<uint8_t[]>(packet_size);
|
||||
size = packet_size;
|
||||
return true;
|
||||
} catch (...) {
|
||||
data.reset();
|
||||
size = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsValid() const {
|
||||
return data && size > 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Vav2Player
|
||||
306
vav2/Vav2Player/Vav2Player/src/Console/HeadlessDecoder.cpp
Normal file
@@ -0,0 +1,306 @@
|
||||
#include "pch.h"
|
||||
#include "HeadlessDecoder.h"
|
||||
#include <chrono>
|
||||
#include <iomanip>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
HeadlessDecoder::HeadlessDecoder() = default;
|
||||
|
||||
HeadlessDecoder::~HeadlessDecoder() = default;
|
||||
|
||||
bool HeadlessDecoder::ProcessFile(const std::string& input_file_path) {
|
||||
try {
|
||||
m_input_file_path = input_file_path;
|
||||
m_start_time = std::chrono::high_resolution_clock::now();
|
||||
|
||||
std::cout << "=== Vav2Player Headless Mode ===" << std::endl;
|
||||
std::cout << "Input file: " << input_file_path << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// 1. 컴포넌트 초기화
|
||||
std::cout << "Step 1: Initializing components..." << std::endl;
|
||||
if (!InitializeComponents()) {
|
||||
std::cerr << "Error: Failed to initialize components" << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cout << "Components initialized successfully" << std::endl;
|
||||
|
||||
// 2. WebM 파일 열기
|
||||
std::cout << "Step 2: Opening WebM file..." << std::endl;
|
||||
if (!OpenWebMFile(input_file_path)) {
|
||||
std::cerr << "Error: Failed to open WebM file" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. 디코더 초기화
|
||||
std::cout << "Step 3: Initializing decoder..." << std::endl;
|
||||
if (!InitializeDecoder()) {
|
||||
std::cerr << "Error: Failed to initialize decoder" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. 출력 초기화
|
||||
std::cout << "Step 4: Initializing output..." << std::endl;
|
||||
if (!InitializeOutput()) {
|
||||
std::cerr << "Error: Failed to initialize output" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 5. 모든 프레임 처리
|
||||
std::cout << "Step 5: Starting frame processing..." << std::endl;
|
||||
if (!ProcessAllFrames()) {
|
||||
std::cerr << "Error: Frame processing failed" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 6. 요약 출력
|
||||
PrintSummary();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
std::cerr << "*** EXCEPTION in ProcessFile: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
catch (...) {
|
||||
std::cerr << "*** UNKNOWN EXCEPTION in ProcessFile" << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool HeadlessDecoder::InitializeComponents() {
|
||||
try {
|
||||
std::cout << " Creating WebMFileReader..." << std::endl;
|
||||
std::cout.flush();
|
||||
m_file_reader = std::make_unique<WebMFileReader>();
|
||||
std::cout << " WebMFileReader created successfully" << std::endl;
|
||||
|
||||
std::cout << " Creating FileOutput..." << std::endl;
|
||||
std::cout.flush();
|
||||
m_file_output = std::make_unique<FileOutput>();
|
||||
std::cout << " FileOutput created successfully" << std::endl;
|
||||
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Exception during component initialization: " << e.what() << std::endl;
|
||||
return false;
|
||||
} catch (...) {
|
||||
std::cerr << "Unknown exception during component initialization" << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool HeadlessDecoder::OpenWebMFile(const std::string& file_path) {
|
||||
std::cout << " Attempting to open file: " << file_path << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
if (!m_file_reader->OpenFile(file_path)) {
|
||||
auto error_code = m_file_reader->GetLastError();
|
||||
std::string error_msg = m_file_reader->GetLastErrorString();
|
||||
std::cerr << "Failed to open WebM file: " << error_msg << std::endl;
|
||||
std::cerr << "Error code: " << static_cast<int>(error_code) << std::endl;
|
||||
|
||||
// 디버깅: 발견된 트랙 정보 출력
|
||||
auto tracks = m_file_reader->GetVideoTracks();
|
||||
if (!tracks.empty()) {
|
||||
std::cout << "Found " << tracks.size() << " video track(s):" << std::endl;
|
||||
for (const auto& track : tracks) {
|
||||
std::cout << " Track #" << track.track_number
|
||||
<< ": " << track.codec_name
|
||||
<< " (" << track.codec_id << ")"
|
||||
<< " " << track.width << "x" << track.height << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 메타데이터 얻기
|
||||
m_metadata = m_file_reader->GetVideoMetadata();
|
||||
|
||||
std::cout << "WebM file opened successfully:" << std::endl;
|
||||
std::cout << " Resolution: " << m_metadata.width << "x" << m_metadata.height << std::endl;
|
||||
std::cout << " Codec: " << m_metadata.codec_name << std::endl;
|
||||
std::cout << " Frame rate: " << m_metadata.frame_rate << " fps" << std::endl;
|
||||
std::cout << " Total frames: " << m_metadata.total_frames << std::endl;
|
||||
std::cout << " Duration: " << std::fixed << std::setprecision(2) << m_metadata.duration_seconds << " seconds" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeadlessDecoder::InitializeDecoder() {
|
||||
std::cout << " Creating decoder for codec: " << m_metadata.codec_name << " (type: " << static_cast<int>(m_metadata.codec_type) << ")" << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
m_decoder = VideoDecoderFactory::CreateDecoder(m_metadata.codec_type);
|
||||
if (!m_decoder) {
|
||||
std::cerr << "Failed to create decoder for codec: " << m_metadata.codec_name << std::endl;
|
||||
std::cerr << "Codec type: " << static_cast<int>(m_metadata.codec_type) << std::endl;
|
||||
return false;
|
||||
}
|
||||
std::cout << " Decoder created successfully" << std::endl;
|
||||
|
||||
std::cout << " Initializing decoder with metadata..." << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
if (!m_decoder->Initialize(m_metadata)) {
|
||||
std::cerr << "Failed to initialize decoder" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Decoder initialized successfully" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeadlessDecoder::InitializeOutput() {
|
||||
std::cout << " Setting up output configuration..." << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
// 출력 디렉토리 설정
|
||||
FileOutput::OutputConfig config;
|
||||
config.format = FileOutput::OutputFormat::BMP;
|
||||
config.output_directory = "output_frames";
|
||||
config.filename_prefix = "frame";
|
||||
config.create_subdirectories = true;
|
||||
config.overwrite_existing = true;
|
||||
|
||||
std::cout << " Applying output config..." << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
m_file_output->SetConfig(config);
|
||||
|
||||
std::cout << " Creating output directory: " << config.output_directory << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
if (!m_file_output->CreateOutputDirectory()) {
|
||||
std::cerr << "Failed to create output directory" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Output configured: " << config.output_directory << "/" << config.filename_prefix << "_XXXXX.bmp" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HeadlessDecoder::ProcessAllFrames() {
|
||||
m_processed_frames = 0;
|
||||
m_successful_frames = 0;
|
||||
m_failed_frames = 0;
|
||||
|
||||
uint64_t total_frames = m_metadata.total_frames;
|
||||
if (total_frames == 0) total_frames = 1000; // 알 수 없는 경우 추정값
|
||||
|
||||
std::cout << " Expected total frames: " << total_frames << std::endl;
|
||||
std::cout << " Starting frame processing loop..." << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
VideoPacket packet;
|
||||
VideoFrame frame;
|
||||
|
||||
std::cout << " Reading first packet..." << std::endl;
|
||||
std::cout.flush();
|
||||
|
||||
while (m_file_reader->ReadNextPacket(packet)) {
|
||||
m_processed_frames++;
|
||||
|
||||
if (m_processed_frames <= 5) { // 처음 5프레임만 상세 로그
|
||||
std::cout << " Processing frame #" << m_processed_frames << std::endl;
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
// 디코딩 시도
|
||||
if (m_decoder->DecodeFrame(packet, frame)) {
|
||||
if (m_processed_frames <= 5) {
|
||||
std::cout << " Decoding successful, saving frame..." << std::endl;
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
// 프레임 저장
|
||||
auto save_result = m_file_output->SaveFrame(frame, m_processed_frames);
|
||||
|
||||
if (save_result.success) {
|
||||
m_successful_frames++;
|
||||
if (m_processed_frames <= 5) {
|
||||
std::cout << " Frame saved successfully" << std::endl;
|
||||
}
|
||||
if (m_verbose && m_successful_frames % 10 == 0) {
|
||||
PrintProgress(m_processed_frames, total_frames);
|
||||
}
|
||||
} else {
|
||||
m_failed_frames++;
|
||||
std::cerr << "Failed to save frame #" << m_processed_frames << ": " << save_result.error_message << std::endl;
|
||||
}
|
||||
} else {
|
||||
m_failed_frames++;
|
||||
if (m_processed_frames <= 5 || m_verbose) {
|
||||
std::cerr << "Failed to decode frame #" << m_processed_frames << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// 진행률 출력 (매 30프레임마다)
|
||||
if (m_processed_frames % 30 == 0) {
|
||||
PrintProgress(m_processed_frames, total_frames);
|
||||
}
|
||||
|
||||
// 처음 몇 프레임만 처리하고 테스트 (디버그용)
|
||||
if (m_processed_frames >= 10) {
|
||||
std::cout << " Stopping after 10 frames for debug..." << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << " Frame processing completed" << std::endl;
|
||||
std::cout << " Total read attempts: " << m_processed_frames << std::endl;
|
||||
std::cout << " Successful decodes: " << m_successful_frames << std::endl;
|
||||
std::cout << " Failed decodes: " << m_failed_frames << std::endl;
|
||||
|
||||
return m_successful_frames > 0;
|
||||
}
|
||||
|
||||
void HeadlessDecoder::PrintProgress(uint64_t current_frame, uint64_t total_frames) {
|
||||
double percentage = 0.0;
|
||||
if (total_frames > 0) {
|
||||
percentage = (double)current_frame / total_frames * 100.0;
|
||||
}
|
||||
|
||||
std::cout << "\rProgress: " << current_frame << "/" << total_frames
|
||||
<< " (" << std::fixed << std::setprecision(1) << percentage << "%) "
|
||||
<< "Success: " << m_successful_frames
|
||||
<< " Failed: " << m_failed_frames << std::flush;
|
||||
}
|
||||
|
||||
void HeadlessDecoder::PrintSummary() {
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - m_start_time);
|
||||
|
||||
std::cout << std::endl << std::endl;
|
||||
std::cout << "=== Processing Summary ===" << std::endl;
|
||||
std::cout << "Total frames processed: " << m_processed_frames << std::endl;
|
||||
std::cout << "Successfully decoded: " << m_successful_frames << std::endl;
|
||||
std::cout << "Failed to decode: " << m_failed_frames << std::endl;
|
||||
std::cout << "Processing time: " << duration.count() << " ms" << std::endl;
|
||||
|
||||
if (duration.count() > 0 && m_successful_frames > 0) {
|
||||
double fps = (double)m_successful_frames / duration.count() * 1000.0;
|
||||
std::cout << "Average FPS: " << std::fixed << std::setprecision(2) << fps << std::endl;
|
||||
}
|
||||
|
||||
if (m_successful_frames > 0) {
|
||||
std::cout << "Output files saved to: output_frames/" << std::endl;
|
||||
std::cout << "Success rate: " << std::fixed << std::setprecision(1)
|
||||
<< (double)m_successful_frames / m_processed_frames * 100.0 << "%" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
if (m_successful_frames == 0) {
|
||||
std::cout << "No frames were successfully processed!" << std::endl;
|
||||
} else {
|
||||
std::cout << "Processing completed successfully!" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
50
vav2/Vav2Player/Vav2Player/src/Console/HeadlessDecoder.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <iostream>
|
||||
#include "src/FileIO/WebMFileReader.h"
|
||||
#include "src/Decoder/IVideoDecoder.h"
|
||||
#include "src/Decoder/VideoDecoderFactory.h"
|
||||
#include "src/Output/FileOutput.h"
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
class HeadlessDecoder {
|
||||
public:
|
||||
HeadlessDecoder();
|
||||
~HeadlessDecoder();
|
||||
|
||||
// 메인 진입점
|
||||
bool ProcessFile(const std::string& input_file_path);
|
||||
|
||||
private:
|
||||
// 초기화
|
||||
bool InitializeComponents();
|
||||
bool OpenWebMFile(const std::string& file_path);
|
||||
bool InitializeDecoder();
|
||||
bool InitializeOutput();
|
||||
|
||||
// 디코딩 프로세스
|
||||
bool ProcessAllFrames();
|
||||
void PrintProgress(uint64_t current_frame, uint64_t total_frames);
|
||||
void PrintSummary();
|
||||
|
||||
// 컴포넌트들
|
||||
std::unique_ptr<WebMFileReader> m_file_reader;
|
||||
std::unique_ptr<IVideoDecoder> m_decoder;
|
||||
std::unique_ptr<FileOutput> m_file_output;
|
||||
|
||||
// 상태 정보
|
||||
std::string m_input_file_path;
|
||||
VideoMetadata m_metadata;
|
||||
uint64_t m_processed_frames = 0;
|
||||
uint64_t m_successful_frames = 0;
|
||||
uint64_t m_failed_frames = 0;
|
||||
bool m_verbose = true;
|
||||
|
||||
// 타이밍
|
||||
std::chrono::high_resolution_clock::time_point m_start_time;
|
||||
};
|
||||
|
||||
} // namespace Vav2Player
|
||||
399
vav2/Vav2Player/Vav2Player/src/Decoder/AV1Decoder.cpp
Normal file
@@ -0,0 +1,399 @@
|
||||
#include "pch.h"
|
||||
#include "AV1Decoder.h"
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
AV1Decoder::AV1Decoder()
|
||||
: m_dav1d_context(nullptr)
|
||||
, m_initialized(false) {
|
||||
// 기본 AV1 설정 초기화
|
||||
m_av1_settings.max_frame_delay = 1;
|
||||
m_av1_settings.num_threads = 0; // 자동
|
||||
m_av1_settings.apply_grain = true;
|
||||
m_av1_settings.all_layers = false;
|
||||
}
|
||||
|
||||
AV1Decoder::~AV1Decoder() {
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
bool AV1Decoder::Initialize(const VideoMetadata& metadata) {
|
||||
if (m_initialized) {
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
m_metadata = metadata;
|
||||
|
||||
if (!InitializeDav1d()) {
|
||||
LogError("Failed to initialize dav1d decoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_initialized = true;
|
||||
ResetStats();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AV1Decoder::Cleanup() {
|
||||
if (!m_initialized) return;
|
||||
|
||||
CleanupDav1d();
|
||||
m_initialized = false;
|
||||
}
|
||||
|
||||
bool AV1Decoder::IsInitialized() const {
|
||||
return m_initialized;
|
||||
}
|
||||
|
||||
bool AV1Decoder::DecodeFrame(const VideoPacket& input_packet, VideoFrame& output_frame) {
|
||||
if (!input_packet.IsValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DecodeFrame(input_packet.data.get(), input_packet.size, output_frame);
|
||||
}
|
||||
|
||||
bool AV1Decoder::DecodeFrame(const uint8_t* packet_data, size_t packet_size, VideoFrame& output_frame) {
|
||||
if (!m_initialized || !packet_data || packet_size == 0) {
|
||||
IncrementDecodeErrors();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto start_time = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// dav1d 입력 데이터 래핑
|
||||
Dav1dData dav1d_data;
|
||||
int res = dav1d_data_wrap(&dav1d_data, packet_data, packet_size, nullptr, nullptr);
|
||||
if (res < 0) {
|
||||
LogError("Failed to wrap input data", res);
|
||||
IncrementDecodeErrors();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 디코더에 데이터 전송
|
||||
res = dav1d_send_data(m_dav1d_context, &dav1d_data);
|
||||
if (res < 0) {
|
||||
LogError("Failed to send data to decoder", res);
|
||||
IncrementDecodeErrors();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 디코딩된 픽처 가져오기
|
||||
Dav1dPicture dav1d_picture;
|
||||
res = dav1d_get_picture(m_dav1d_context, &dav1d_picture);
|
||||
if (res < 0) {
|
||||
if (res != DAV1D_ERR(EAGAIN)) {
|
||||
LogError("Failed to get decoded picture", res);
|
||||
IncrementDecodeErrors();
|
||||
}
|
||||
// EAGAIN은 더 많은 데이터가 필요한 경우이므로 에러가 아님
|
||||
return res == DAV1D_ERR(EAGAIN);
|
||||
}
|
||||
|
||||
// dav1d 픽처를 VideoFrame으로 변환
|
||||
if (!ConvertDav1dPicture(dav1d_picture, output_frame)) {
|
||||
dav1d_picture_unref(&dav1d_picture);
|
||||
IncrementDecodeErrors();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 픽처 참조 해제
|
||||
dav1d_picture_unref(&dav1d_picture);
|
||||
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
|
||||
double decode_time_ms = duration.count() / 1000.0;
|
||||
|
||||
UpdateDecodingStats(decode_time_ms, packet_size);
|
||||
IncrementFramesDecoded();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AV1Decoder::Reset() {
|
||||
if (!m_initialized) return false;
|
||||
|
||||
// dav1d 컨텍스트의 내부 버퍼 플러시
|
||||
dav1d_flush(m_dav1d_context);
|
||||
|
||||
// 통계 리셋
|
||||
ResetStats();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AV1Decoder::Flush() {
|
||||
if (!m_initialized) return false;
|
||||
|
||||
// 디코더에서 남은 프레임들을 모두 가져오기
|
||||
// 스트림 끝에서 지연된 프레임들을 처리하기 위함
|
||||
while (true) {
|
||||
Dav1dPicture picture;
|
||||
int res = dav1d_get_picture(m_dav1d_context, &picture);
|
||||
if (res < 0) {
|
||||
// 더 이상 프레임이 없거나 에러 발생 시 종료
|
||||
break;
|
||||
}
|
||||
|
||||
// 프레임이 있으면 참조 해제 (현재는 무시)
|
||||
// 실제 스트리밍에서는 이 프레임들을 처리해야 함
|
||||
dav1d_picture_unref(&picture);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string AV1Decoder::GetCodecName() const {
|
||||
return "AV1";
|
||||
}
|
||||
|
||||
VideoCodecType AV1Decoder::GetCodecType() const {
|
||||
return VideoCodecType::AV1;
|
||||
}
|
||||
|
||||
std::string AV1Decoder::GetVersion() const {
|
||||
return AV1Utils::GetDav1dVersion();
|
||||
}
|
||||
|
||||
IVideoDecoder::DecoderStats AV1Decoder::GetStats() const {
|
||||
return m_stats;
|
||||
}
|
||||
|
||||
void AV1Decoder::ResetStats() {
|
||||
m_stats = {};
|
||||
}
|
||||
|
||||
bool AV1Decoder::SetOption(const std::string& key, const std::string& value) {
|
||||
// AV1 특화 옵션 처리
|
||||
if (key == "max_frame_delay") {
|
||||
try {
|
||||
m_av1_settings.max_frame_delay = std::stoi(value);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (key == "num_threads") {
|
||||
try {
|
||||
m_av1_settings.num_threads = std::stoi(value);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (key == "apply_grain") {
|
||||
m_av1_settings.apply_grain = (value == "true" || value == "1");
|
||||
return true;
|
||||
}
|
||||
else if (key == "all_layers") {
|
||||
m_av1_settings.all_layers = (value == "true" || value == "1");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // 지원하지 않는 옵션
|
||||
}
|
||||
|
||||
std::string AV1Decoder::GetOption(const std::string& key) const {
|
||||
if (key == "max_frame_delay") {
|
||||
return std::to_string(m_av1_settings.max_frame_delay);
|
||||
}
|
||||
else if (key == "num_threads") {
|
||||
return std::to_string(m_av1_settings.num_threads);
|
||||
}
|
||||
else if (key == "apply_grain") {
|
||||
return m_av1_settings.apply_grain ? "true" : "false";
|
||||
}
|
||||
else if (key == "all_layers") {
|
||||
return m_av1_settings.all_layers ? "true" : "false";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
void AV1Decoder::SetAV1Settings(const AV1Settings& settings) {
|
||||
m_av1_settings = settings;
|
||||
|
||||
if (m_initialized) {
|
||||
// 런타임 설정 변경 시 dav1d 설정 업데이트
|
||||
SetupDav1dSettings();
|
||||
}
|
||||
}
|
||||
|
||||
AV1Decoder::AV1Settings AV1Decoder::GetAV1Settings() const {
|
||||
return m_av1_settings;
|
||||
}
|
||||
|
||||
bool AV1Decoder::InitializeDav1d() {
|
||||
// dav1d 기본 설정 초기화
|
||||
dav1d_default_settings(&m_dav1d_settings);
|
||||
|
||||
if (!SetupDav1dSettings()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// dav1d 컨텍스트 생성
|
||||
int res = dav1d_open(&m_dav1d_context, &m_dav1d_settings);
|
||||
if (res < 0) {
|
||||
LogError("Failed to open dav1d context", res);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AV1Decoder::CleanupDav1d() {
|
||||
if (m_dav1d_context) {
|
||||
dav1d_close(&m_dav1d_context);
|
||||
m_dav1d_context = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool AV1Decoder::SetupDav1dSettings() {
|
||||
// AV1 설정을 dav1d 설정으로 변환
|
||||
m_dav1d_settings.max_frame_delay = m_av1_settings.max_frame_delay;
|
||||
m_dav1d_settings.n_threads = m_av1_settings.num_threads;
|
||||
m_dav1d_settings.apply_grain = m_av1_settings.apply_grain;
|
||||
m_dav1d_settings.all_layers = m_av1_settings.all_layers;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AV1Decoder::ConvertDav1dPicture(const Dav1dPicture& dav1d_pic, VideoFrame& output_frame) {
|
||||
output_frame.Reset();
|
||||
|
||||
// 기본 정보 설정
|
||||
output_frame.width = static_cast<uint32_t>(dav1d_pic.p.w);
|
||||
output_frame.height = static_cast<uint32_t>(dav1d_pic.p.h);
|
||||
output_frame.color_space = ConvertDav1dPixelFormat(dav1d_pic);
|
||||
|
||||
// YUV420P 데이터 할당
|
||||
if (!output_frame.AllocateYUV420P(output_frame.width, output_frame.height)) {
|
||||
LogError("Failed to allocate YUV420P frame memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
// dav1d 픽처에서 YUV 데이터 포인터 가져오기
|
||||
const uint8_t* src_y = static_cast<const uint8_t*>(dav1d_pic.data[0]);
|
||||
const uint8_t* src_u = static_cast<const uint8_t*>(dav1d_pic.data[1]);
|
||||
const uint8_t* src_v = static_cast<const uint8_t*>(dav1d_pic.data[2]);
|
||||
|
||||
if (!src_y || !src_u || !src_v) {
|
||||
LogError("Invalid dav1d picture data pointers");
|
||||
return false;
|
||||
}
|
||||
|
||||
// stride 정보 가져오기
|
||||
ptrdiff_t src_y_stride = dav1d_pic.stride[0];
|
||||
ptrdiff_t src_u_stride = dav1d_pic.stride[1];
|
||||
ptrdiff_t src_v_stride = dav1d_pic.stride[1]; // U/V는 같은 stride
|
||||
|
||||
// Y plane 복사 (line by line)
|
||||
uint8_t* dst_y = output_frame.y_plane.get();
|
||||
for (uint32_t y = 0; y < output_frame.height; ++y) {
|
||||
memcpy(dst_y + y * output_frame.y_stride,
|
||||
src_y + y * src_y_stride,
|
||||
output_frame.width);
|
||||
}
|
||||
|
||||
// U plane 복사 (line by line)
|
||||
uint8_t* dst_u = output_frame.u_plane.get();
|
||||
uint32_t uv_height = output_frame.height / 2;
|
||||
uint32_t uv_width = output_frame.width / 2;
|
||||
for (uint32_t y = 0; y < uv_height; ++y) {
|
||||
memcpy(dst_u + y * output_frame.u_stride,
|
||||
src_u + y * src_u_stride,
|
||||
uv_width);
|
||||
}
|
||||
|
||||
// V plane 복사 (line by line)
|
||||
uint8_t* dst_v = output_frame.v_plane.get();
|
||||
for (uint32_t y = 0; y < uv_height; ++y) {
|
||||
memcpy(dst_v + y * output_frame.v_stride,
|
||||
src_v + y * src_v_stride,
|
||||
uv_width);
|
||||
}
|
||||
|
||||
// 프레임 메타데이터 설정
|
||||
output_frame.is_valid = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AV1Decoder::UpdateDecodingStats(double decode_time_ms, size_t input_bytes) {
|
||||
UpdateDecodeTime(decode_time_ms);
|
||||
AddBytesProcessed(input_bytes);
|
||||
}
|
||||
|
||||
ColorSpace AV1Decoder::ConvertDav1dPixelFormat(const Dav1dPicture& pic) {
|
||||
// dav1d 픽셀 포맷에 따른 ColorSpace 변환
|
||||
switch (pic.p.layout) {
|
||||
case DAV1D_PIXEL_LAYOUT_I420:
|
||||
return ColorSpace::YUV420P;
|
||||
case DAV1D_PIXEL_LAYOUT_I422:
|
||||
return ColorSpace::YUV422P;
|
||||
case DAV1D_PIXEL_LAYOUT_I444:
|
||||
return ColorSpace::YUV444P;
|
||||
default:
|
||||
// 지원하지 않는 포맷은 YUV420P로 기본 설정
|
||||
LogError("Unsupported pixel layout: " + std::to_string(static_cast<int>(pic.p.layout)));
|
||||
return ColorSpace::YUV420P;
|
||||
}
|
||||
}
|
||||
|
||||
std::string AV1Decoder::GetDav1dErrorString(int error_code) {
|
||||
// dav1d 에러 코드를 의미있는 문자열로 변환
|
||||
switch (error_code) {
|
||||
case DAV1D_ERR(EAGAIN):
|
||||
return "EAGAIN (더 많은 데이터 필요)";
|
||||
case DAV1D_ERR(EINVAL):
|
||||
return "EINVAL (잘못된 매개변수)";
|
||||
case DAV1D_ERR(ENOMEM):
|
||||
return "ENOMEM (메모리 부족)";
|
||||
case DAV1D_ERR(EIO):
|
||||
return "EIO (입출력 오류)";
|
||||
default:
|
||||
return "dav1d error: " + std::to_string(error_code);
|
||||
}
|
||||
}
|
||||
|
||||
void AV1Decoder::LogError(const std::string& message, int error_code) {
|
||||
std::string full_message = "[AV1Decoder] " + message;
|
||||
if (error_code != 0) {
|
||||
full_message += " (" + GetDav1dErrorString(error_code) + ")";
|
||||
}
|
||||
|
||||
// TODO: 실제 로깅 시스템 연동
|
||||
std::cerr << full_message << std::endl;
|
||||
}
|
||||
|
||||
// AV1Utils 구현
|
||||
namespace AV1Utils {
|
||||
|
||||
OBUType GetOBUType(const uint8_t* data, size_t size) {
|
||||
// TODO: AV1 OBU 헤더 파싱 구현
|
||||
if (!data || size < 1) return OBUType::UNKNOWN;
|
||||
|
||||
uint8_t obu_type = (data[0] >> 3) & 0x0F;
|
||||
return static_cast<OBUType>(obu_type);
|
||||
}
|
||||
|
||||
bool IsKeyFrame(const uint8_t* data, size_t size) {
|
||||
// TODO: AV1 키프레임 감지 로직 구현
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetDav1dVersion() {
|
||||
// dav1d 라이브러리 버전 정보 가져오기
|
||||
const char* version = dav1d_version();
|
||||
return version ? std::string("dav1d ") + version : "dav1d (version unknown)";
|
||||
}
|
||||
|
||||
std::string GetDav1dCopyright() {
|
||||
return "Copyright (c) 2018-2024, VideoLAN and dav1d authors";
|
||||
}
|
||||
|
||||
} // namespace AV1Utils
|
||||
|
||||
} // namespace Vav2Player
|
||||
103
vav2/Vav2Player/Vav2Player/src/Decoder/AV1Decoder.h
Normal file
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
#include "IVideoDecoder.h"
|
||||
#include <dav1d.h>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// AV1 비디오 디코더 구현 클래스
|
||||
// dav1d 라이브러리를 사용하여 AV1 비디오 스트림을 YUV 프레임으로 디코딩
|
||||
class AV1Decoder : public IVideoDecoder {
|
||||
public:
|
||||
AV1Decoder();
|
||||
~AV1Decoder() override;
|
||||
|
||||
// 복사 방지
|
||||
AV1Decoder(const AV1Decoder&) = delete;
|
||||
AV1Decoder& operator=(const AV1Decoder&) = delete;
|
||||
|
||||
// IVideoDecoder 인터페이스 구현
|
||||
bool Initialize(const VideoMetadata& metadata) override;
|
||||
void Cleanup() override;
|
||||
bool IsInitialized() const override;
|
||||
|
||||
bool DecodeFrame(const VideoPacket& input_packet, VideoFrame& output_frame) override;
|
||||
bool DecodeFrame(const uint8_t* packet_data, size_t packet_size, VideoFrame& output_frame) override;
|
||||
|
||||
bool Reset() override;
|
||||
bool Flush() override;
|
||||
|
||||
std::string GetCodecName() const override;
|
||||
VideoCodecType GetCodecType() const override;
|
||||
std::string GetVersion() const override;
|
||||
|
||||
DecoderStats GetStats() const override;
|
||||
void ResetStats() override;
|
||||
|
||||
// AV1 특화 옵션들
|
||||
bool SetOption(const std::string& key, const std::string& value) override;
|
||||
std::string GetOption(const std::string& key) const override;
|
||||
|
||||
// AV1 전용 메서드들
|
||||
struct AV1Settings {
|
||||
int max_frame_delay = 1; // 최대 프레임 지연 (낮을수록 지연 적음)
|
||||
int num_threads = 0; // 디코딩 스레드 수 (0 = 자동)
|
||||
bool apply_grain = true; // 필름 그레인 합성 적용
|
||||
bool all_layers = false; // 모든 spatial/temporal 레이어 디코딩
|
||||
};
|
||||
|
||||
void SetAV1Settings(const AV1Settings& settings);
|
||||
AV1Settings GetAV1Settings() const;
|
||||
|
||||
private:
|
||||
// dav1d 관련 멤버들
|
||||
Dav1dContext* m_dav1d_context;
|
||||
Dav1dSettings m_dav1d_settings;
|
||||
AV1Settings m_av1_settings;
|
||||
|
||||
// 초기화 상태
|
||||
bool m_initialized;
|
||||
VideoMetadata m_metadata;
|
||||
|
||||
// 성능 측정을 위한 멤버들
|
||||
std::chrono::high_resolution_clock::time_point m_decode_start_time;
|
||||
|
||||
// 내부 helper 메서드들
|
||||
bool InitializeDav1d();
|
||||
void CleanupDav1d();
|
||||
bool SetupDav1dSettings();
|
||||
|
||||
bool ConvertDav1dPicture(const Dav1dPicture& dav1d_pic, VideoFrame& output_frame);
|
||||
void UpdateDecodingStats(double decode_time_ms, size_t input_bytes);
|
||||
|
||||
// dav1d 픽셀 포맷을 VideoTypes 포맷으로 변환
|
||||
ColorSpace ConvertDav1dPixelFormat(const Dav1dPicture& pic);
|
||||
|
||||
// 에러 처리
|
||||
std::string GetDav1dErrorString(int error_code);
|
||||
void LogError(const std::string& message, int error_code = 0);
|
||||
};
|
||||
|
||||
// AV1 관련 유틸리티 함수들
|
||||
namespace AV1Utils {
|
||||
// AV1 OBU (Open Bitstream Unit) 타입 분석
|
||||
enum class OBUType {
|
||||
SEQUENCE_HEADER = 1,
|
||||
TEMPORAL_DELIMITER = 2,
|
||||
FRAME_HEADER = 3,
|
||||
FRAME = 6,
|
||||
TILE_GROUP = 4,
|
||||
METADATA = 5,
|
||||
UNKNOWN = -1
|
||||
};
|
||||
|
||||
OBUType GetOBUType(const uint8_t* data, size_t size);
|
||||
bool IsKeyFrame(const uint8_t* data, size_t size);
|
||||
|
||||
// dav1d 버전 정보
|
||||
std::string GetDav1dVersion();
|
||||
std::string GetDav1dCopyright();
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
69
vav2/Vav2Player/Vav2Player/src/Decoder/IVideoDecoder.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
#include "../Common/VideoTypes.h"
|
||||
#include <string>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 비디오 디코더 인터페이스
|
||||
// 다양한 코덱(AV1, VP9, H.264 등)에 대한 공통 인터페이스 제공
|
||||
class IVideoDecoder {
|
||||
public:
|
||||
virtual ~IVideoDecoder() = default;
|
||||
|
||||
// 초기화 및 해제
|
||||
virtual bool Initialize(const VideoMetadata& metadata) = 0;
|
||||
virtual void Cleanup() = 0;
|
||||
virtual bool IsInitialized() const = 0;
|
||||
|
||||
// 디코딩 핵심 기능
|
||||
virtual bool DecodeFrame(const VideoPacket& input_packet, VideoFrame& output_frame) = 0;
|
||||
|
||||
// 추가 디코딩 옵션 (일부 디코더에서 사용)
|
||||
virtual bool DecodeFrame(const uint8_t* packet_data, size_t packet_size, VideoFrame& output_frame) = 0;
|
||||
|
||||
// 디코더 상태 관리
|
||||
virtual bool Reset() = 0; // 디코더 상태 초기화
|
||||
virtual bool Flush() = 0; // 남은 프레임들 출력
|
||||
|
||||
// 디코더 정보
|
||||
virtual std::string GetCodecName() const = 0;
|
||||
virtual VideoCodecType GetCodecType() const = 0;
|
||||
virtual std::string GetVersion() const = 0;
|
||||
|
||||
// 성능 및 통계 정보
|
||||
struct DecoderStats {
|
||||
uint64_t frames_decoded = 0;
|
||||
uint64_t frames_dropped = 0;
|
||||
uint64_t decode_errors = 0;
|
||||
double avg_decode_time_ms = 0.0;
|
||||
uint64_t bytes_processed = 0;
|
||||
};
|
||||
|
||||
virtual DecoderStats GetStats() const = 0;
|
||||
virtual void ResetStats() = 0;
|
||||
|
||||
// 디코더별 특화 설정 (옵션)
|
||||
virtual bool SetOption(const std::string& key, const std::string& value) {
|
||||
return false; // 기본 구현: 설정 미지원
|
||||
}
|
||||
virtual std::string GetOption(const std::string& key) const {
|
||||
return ""; // 기본 구현: 설정 미지원
|
||||
}
|
||||
|
||||
protected:
|
||||
// 파생 클래스에서 통계 업데이트용
|
||||
mutable DecoderStats m_stats{};
|
||||
|
||||
void UpdateDecodeTime(double decode_time_ms) {
|
||||
m_stats.avg_decode_time_ms =
|
||||
(m_stats.avg_decode_time_ms * m_stats.frames_decoded + decode_time_ms) /
|
||||
(m_stats.frames_decoded + 1);
|
||||
}
|
||||
|
||||
void IncrementFramesDecoded() { ++m_stats.frames_decoded; }
|
||||
void IncrementFramesDropped() { ++m_stats.frames_dropped; }
|
||||
void IncrementDecodeErrors() { ++m_stats.decode_errors; }
|
||||
void AddBytesProcessed(size_t bytes) { m_stats.bytes_processed += bytes; }
|
||||
};
|
||||
|
||||
} // namespace Vav2Player
|
||||
189
vav2/Vav2Player/Vav2Player/src/Decoder/VideoDecoderFactory.cpp
Normal file
@@ -0,0 +1,189 @@
|
||||
#include "pch.h"
|
||||
#include "VideoDecoderFactory.h"
|
||||
#include "AV1Decoder.h"
|
||||
// #include "VP9Decoder.h" // TODO: VP9 구현시 활성화
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 정적 멤버 초기화
|
||||
bool VideoDecoderFactory::s_av1_available = false;
|
||||
bool VideoDecoderFactory::s_vp9_available = false;
|
||||
bool VideoDecoderFactory::s_factory_initialized = false;
|
||||
|
||||
std::unique_ptr<IVideoDecoder> VideoDecoderFactory::CreateDecoder(VideoCodecType codec_type) {
|
||||
if (!s_factory_initialized) {
|
||||
InitializeFactory();
|
||||
}
|
||||
|
||||
switch (codec_type) {
|
||||
case VideoCodecType::AV1:
|
||||
if (s_av1_available) {
|
||||
return std::make_unique<AV1Decoder>();
|
||||
}
|
||||
break;
|
||||
|
||||
case VideoCodecType::VP9:
|
||||
// TODO: VP9 구현시 활성화
|
||||
// if (s_vp9_available) {
|
||||
// return std::make_unique<VP9Decoder>();
|
||||
// }
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<IVideoDecoder> VideoDecoderFactory::CreateDecoderFromCodecId(const std::string& codec_id) {
|
||||
VideoCodecType codec_type = DetectCodecTypeFromId(codec_id);
|
||||
return CreateDecoder(codec_type);
|
||||
}
|
||||
|
||||
VideoCodecType VideoDecoderFactory::DetectCodecTypeFromId(const std::string& codec_id) {
|
||||
if (codec_id == DecoderUtils::CodecIds::AV1) return VideoCodecType::AV1;
|
||||
if (codec_id == DecoderUtils::CodecIds::VP9) return VideoCodecType::VP9;
|
||||
if (codec_id == DecoderUtils::CodecIds::VP8) return VideoCodecType::VP8;
|
||||
if (codec_id == DecoderUtils::CodecIds::H264) return VideoCodecType::H264;
|
||||
if (codec_id == DecoderUtils::CodecIds::H265) return VideoCodecType::H265;
|
||||
|
||||
return VideoCodecType::AV1; // 기본값
|
||||
}
|
||||
|
||||
std::vector<VideoDecoderFactory::DecoderInfo> VideoDecoderFactory::GetSupportedDecoders() {
|
||||
if (!s_factory_initialized) {
|
||||
InitializeFactory();
|
||||
}
|
||||
|
||||
std::vector<DecoderInfo> decoders;
|
||||
|
||||
decoders.push_back({
|
||||
VideoCodecType::AV1,
|
||||
"AV1",
|
||||
"AV1 video decoder using dav1d library",
|
||||
s_av1_available
|
||||
});
|
||||
|
||||
decoders.push_back({
|
||||
VideoCodecType::VP9,
|
||||
"VP9",
|
||||
"VP9 video decoder (TODO: not implemented yet)",
|
||||
s_vp9_available
|
||||
});
|
||||
|
||||
return decoders;
|
||||
}
|
||||
|
||||
bool VideoDecoderFactory::IsCodecSupported(VideoCodecType codec_type) {
|
||||
if (!s_factory_initialized) {
|
||||
InitializeFactory();
|
||||
}
|
||||
|
||||
switch (codec_type) {
|
||||
case VideoCodecType::AV1: return s_av1_available;
|
||||
case VideoCodecType::VP9: return s_vp9_available;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool VideoDecoderFactory::IsCodecSupported(const std::string& codec_id) {
|
||||
VideoCodecType codec_type = DetectCodecTypeFromId(codec_id);
|
||||
return IsCodecSupported(codec_type);
|
||||
}
|
||||
|
||||
void VideoDecoderFactory::InitializeFactory() {
|
||||
if (s_factory_initialized) return;
|
||||
|
||||
// 각 디코더의 가용성 체크
|
||||
s_av1_available = CheckAV1DecoderAvailability();
|
||||
s_vp9_available = CheckVP9DecoderAvailability();
|
||||
|
||||
s_factory_initialized = true;
|
||||
}
|
||||
|
||||
void VideoDecoderFactory::CleanupFactory() {
|
||||
s_factory_initialized = false;
|
||||
s_av1_available = false;
|
||||
s_vp9_available = false;
|
||||
}
|
||||
|
||||
std::string VideoDecoderFactory::GetDecoderVersion(VideoCodecType codec_type) {
|
||||
switch (codec_type) {
|
||||
case VideoCodecType::AV1:
|
||||
return "dav1d 1.0+"; // TODO: 실제 버전 정보 가져오기
|
||||
case VideoCodecType::VP9:
|
||||
return "Not implemented"; // TODO: VP9 구현시
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string VideoDecoderFactory::GetDecoderDescription(VideoCodecType codec_type) {
|
||||
switch (codec_type) {
|
||||
case VideoCodecType::AV1:
|
||||
return "High-performance AV1 decoder";
|
||||
case VideoCodecType::VP9:
|
||||
return "VP9 decoder (TODO)";
|
||||
default:
|
||||
return "Unknown decoder";
|
||||
}
|
||||
}
|
||||
|
||||
bool VideoDecoderFactory::CheckAV1DecoderAvailability() {
|
||||
// TODO: 실제 dav1d 라이브러리 로드 체크
|
||||
// 현재는 항상 사용 가능하다고 가정
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VideoDecoderFactory::CheckVP9DecoderAvailability() {
|
||||
// TODO: VP9 디코더 구현 후 활성화
|
||||
return false;
|
||||
}
|
||||
|
||||
// DecoderUtils 구현
|
||||
namespace DecoderUtils {
|
||||
|
||||
std::string GetFriendlyCodecName(const std::string& codec_id) {
|
||||
if (codec_id == CodecIds::AV1) return "AV1";
|
||||
if (codec_id == CodecIds::VP9) return "VP9";
|
||||
if (codec_id == CodecIds::VP8) return "VP8";
|
||||
if (codec_id == CodecIds::H264) return "H.264/AVC";
|
||||
if (codec_id == CodecIds::H265) return "H.265/HEVC";
|
||||
return "Unknown (" + codec_id + ")";
|
||||
}
|
||||
|
||||
std::string GetFriendlyCodecName(VideoCodecType codec_type) {
|
||||
switch (codec_type) {
|
||||
case VideoCodecType::AV1: return "AV1";
|
||||
case VideoCodecType::VP9: return "VP9";
|
||||
case VideoCodecType::VP8: return "VP8";
|
||||
case VideoCodecType::H264: return "H.264/AVC";
|
||||
case VideoCodecType::H265: return "H.265/HEVC";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string CodecTypeToString(VideoCodecType codec_type) {
|
||||
switch (codec_type) {
|
||||
case VideoCodecType::AV1: return "AV1";
|
||||
case VideoCodecType::VP9: return "VP9";
|
||||
case VideoCodecType::VP8: return "VP8";
|
||||
case VideoCodecType::H264: return "H264";
|
||||
case VideoCodecType::H265: return "H265";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
VideoCodecType StringToCodecType(const std::string& codec_string) {
|
||||
if (codec_string == "AV1") return VideoCodecType::AV1;
|
||||
if (codec_string == "VP9") return VideoCodecType::VP9;
|
||||
if (codec_string == "VP8") return VideoCodecType::VP8;
|
||||
if (codec_string == "H264") return VideoCodecType::H264;
|
||||
if (codec_string == "H265") return VideoCodecType::H265;
|
||||
return VideoCodecType::AV1; // 기본값
|
||||
}
|
||||
|
||||
} // namespace DecoderUtils
|
||||
|
||||
} // namespace Vav2Player
|
||||
82
vav2/Vav2Player/Vav2Player/src/Decoder/VideoDecoderFactory.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
#include "IVideoDecoder.h"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 비디오 디코더 팩토리 클래스
|
||||
// 코덱 타입에 따라 적절한 디코더 인스턴스를 생성
|
||||
class VideoDecoderFactory {
|
||||
public:
|
||||
// 지원되는 디코더 정보
|
||||
struct DecoderInfo {
|
||||
VideoCodecType codec_type;
|
||||
std::string codec_name;
|
||||
std::string description;
|
||||
bool is_available; // 현재 사용 가능한지 (라이브러리 로드 여부 등)
|
||||
};
|
||||
|
||||
// 디코더 생성 (코덱 타입 기반)
|
||||
static std::unique_ptr<IVideoDecoder> CreateDecoder(VideoCodecType codec_type);
|
||||
|
||||
// 디코더 생성 (코덱 ID 문자열 기반 - WebM에서 사용)
|
||||
static std::unique_ptr<IVideoDecoder> CreateDecoderFromCodecId(const std::string& codec_id);
|
||||
|
||||
// 코덱 ID 문자열을 VideoCodecType으로 변환
|
||||
static VideoCodecType DetectCodecTypeFromId(const std::string& codec_id);
|
||||
|
||||
// 지원되는 모든 디코더 목록 반환
|
||||
static std::vector<DecoderInfo> GetSupportedDecoders();
|
||||
|
||||
// 특정 코덱이 지원되는지 확인
|
||||
static bool IsCodecSupported(VideoCodecType codec_type);
|
||||
static bool IsCodecSupported(const std::string& codec_id);
|
||||
|
||||
// 디코더 가용성 체크 (라이브러리 로드 상태 등 확인)
|
||||
static void InitializeFactory(); // 앱 시작시 호출
|
||||
static void CleanupFactory(); // 앱 종료시 호출
|
||||
|
||||
// 디코더별 추가 정보
|
||||
static std::string GetDecoderVersion(VideoCodecType codec_type);
|
||||
static std::string GetDecoderDescription(VideoCodecType codec_type);
|
||||
|
||||
private:
|
||||
// 팩토리는 정적 클래스로 사용
|
||||
VideoDecoderFactory() = delete;
|
||||
~VideoDecoderFactory() = delete;
|
||||
VideoDecoderFactory(const VideoDecoderFactory&) = delete;
|
||||
VideoDecoderFactory& operator=(const VideoDecoderFactory&) = delete;
|
||||
|
||||
// 내부 helper 함수들
|
||||
static bool CheckAV1DecoderAvailability();
|
||||
static bool CheckVP9DecoderAvailability(); // TODO: VP9 구현시
|
||||
|
||||
// 디코더 가용성 상태 캐시
|
||||
static bool s_av1_available;
|
||||
static bool s_vp9_available; // TODO: VP9 구현시
|
||||
static bool s_factory_initialized;
|
||||
};
|
||||
|
||||
// 편의 함수들
|
||||
namespace DecoderUtils {
|
||||
// WebM 코덱 ID를 사람이 읽을 수 있는 이름으로 변환
|
||||
std::string GetFriendlyCodecName(const std::string& codec_id);
|
||||
std::string GetFriendlyCodecName(VideoCodecType codec_type);
|
||||
|
||||
// 코덱 타입을 문자열로 변환
|
||||
std::string CodecTypeToString(VideoCodecType codec_type);
|
||||
VideoCodecType StringToCodecType(const std::string& codec_string);
|
||||
|
||||
// 널리 사용되는 WebM 코덱 ID들
|
||||
namespace CodecIds {
|
||||
constexpr const char* AV1 = "V_AV01";
|
||||
constexpr const char* VP9 = "V_VP9";
|
||||
constexpr const char* VP8 = "V_VP8";
|
||||
constexpr const char* H264 = "V_MPEG4/ISO/AVC";
|
||||
constexpr const char* H265 = "V_MPEGH/ISO/HEVC";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
826
vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.cpp
Normal file
@@ -0,0 +1,826 @@
|
||||
#include "pch.h"
|
||||
#include "WebMFileReader.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// libwebm IMkvReader 구현 클래스
|
||||
class WebMFileReader::MkvReader : public mkvparser::IMkvReader {
|
||||
public:
|
||||
MkvReader() : m_file(nullptr) {}
|
||||
~MkvReader() { Close(); }
|
||||
|
||||
bool Open(const std::string& file_path) {
|
||||
Close();
|
||||
|
||||
errno_t err = fopen_s(&m_file, file_path.c_str(), "rb");
|
||||
if (err != 0 || !m_file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 파일 크기 계산
|
||||
_fseeki64(m_file, 0, SEEK_END);
|
||||
m_file_size = _ftelli64(m_file);
|
||||
_fseeki64(m_file, 0, SEEK_SET);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Close() {
|
||||
if (m_file) {
|
||||
fclose(m_file);
|
||||
m_file = nullptr;
|
||||
}
|
||||
m_file_size = 0;
|
||||
}
|
||||
|
||||
// IMkvReader 인터페이스 구현
|
||||
int Read(long long pos, long len, unsigned char* buf) override {
|
||||
if (!m_file || !buf || len < 0) return -1;
|
||||
|
||||
if (_fseeki64(m_file, pos, SEEK_SET) != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const size_t bytes_read = fread(buf, 1, static_cast<size_t>(len), m_file);
|
||||
|
||||
// libwebm IMkvReader 규약:
|
||||
// - Return 0: 성공 (요청한 모든 바이트를 읽음)
|
||||
// - Return positive: 언더플로우 (읽을 수 있는 바이트가 부족)
|
||||
// - Return negative: 에러
|
||||
if (bytes_read == static_cast<size_t>(len)) {
|
||||
return 0; // 성공: 요청한 모든 바이트를 읽음
|
||||
} else if (bytes_read < static_cast<size_t>(len)) {
|
||||
return static_cast<int>(len - bytes_read); // 언더플로우: 부족한 바이트 수 반환
|
||||
} else {
|
||||
return -1; // 예상치 못한 상황
|
||||
}
|
||||
}
|
||||
|
||||
int Length(long long* total, long long* available) override {
|
||||
if (!m_file) return -1;
|
||||
|
||||
if (total) *total = m_file_size;
|
||||
if (available) *available = m_file_size; // 전체 파일 사용 가능
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::FILE* m_file;
|
||||
long long m_file_size = 0;
|
||||
};
|
||||
|
||||
// WebMFileReader 내부 상태 관리
|
||||
struct WebMFileReader::InternalState {
|
||||
std::unique_ptr<MkvReader> reader;
|
||||
std::unique_ptr<mkvparser::Segment> segment;
|
||||
|
||||
// 현재 상태
|
||||
std::string file_path;
|
||||
VideoMetadata metadata;
|
||||
std::vector<VideoTrackInfo> video_tracks;
|
||||
uint64_t selected_track_number = 0;
|
||||
|
||||
// 현재 읽기 위치
|
||||
const mkvparser::Cluster* current_cluster = nullptr;
|
||||
const mkvparser::BlockEntry* current_block_entry = nullptr;
|
||||
uint64_t current_frame_index = 0;
|
||||
double current_timestamp = 0.0;
|
||||
bool end_of_file = false;
|
||||
|
||||
// 에러 처리
|
||||
ErrorCode last_error = ErrorCode::Success;
|
||||
std::string last_error_message;
|
||||
|
||||
InternalState() : reader(std::make_unique<MkvReader>()) {}
|
||||
};
|
||||
|
||||
WebMFileReader::WebMFileReader() : m_state(std::make_unique<InternalState>()) {
|
||||
}
|
||||
|
||||
WebMFileReader::~WebMFileReader() {
|
||||
CloseFile();
|
||||
}
|
||||
|
||||
bool WebMFileReader::OpenFile(const std::string& file_path) {
|
||||
CloseFile();
|
||||
|
||||
if (file_path.empty()) {
|
||||
SetLastError(ErrorCode::FileNotFound, "File path is empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 파일 열기
|
||||
if (!m_state->reader->Open(file_path)) {
|
||||
SetLastError(ErrorCode::FileNotFound, "Cannot open file: " + file_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state->file_path = file_path;
|
||||
|
||||
// libwebm parser 초기화
|
||||
if (!InitializeParser()) {
|
||||
CloseFile();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 비디오 트랙 추출
|
||||
if (!ExtractVideoTracks()) {
|
||||
CloseFile();
|
||||
SetLastError(ErrorCode::NoVideoTrack, "No supported video tracks found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 첫 번째 지원되는 비디오 트랙 자동 선택
|
||||
for (const auto& track : m_state->video_tracks) {
|
||||
if (IsVideoCodecSupported(track.codec_id)) {
|
||||
SelectVideoTrack(track.track_number);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_state->selected_track_number == 0) {
|
||||
CloseFile();
|
||||
SetLastError(ErrorCode::UnsupportedCodec, "No supported video codecs found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 메타데이터 추출
|
||||
if (!ExtractVideoMetadata()) {
|
||||
CloseFile();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 읽기 위치 초기화
|
||||
Reset();
|
||||
|
||||
SetLastError(ErrorCode::Success);
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebMFileReader::CloseFile() {
|
||||
if (m_state) {
|
||||
m_state->segment.reset();
|
||||
m_state->reader->Close();
|
||||
m_state->video_tracks.clear();
|
||||
m_state->selected_track_number = 0;
|
||||
m_state->current_cluster = nullptr;
|
||||
m_state->current_block_entry = nullptr;
|
||||
m_state->current_frame_index = 0;
|
||||
m_state->current_timestamp = 0.0;
|
||||
m_state->end_of_file = false;
|
||||
m_state->file_path.clear();
|
||||
}
|
||||
}
|
||||
|
||||
bool WebMFileReader::IsFileOpen() const {
|
||||
return m_state && m_state->segment && !m_state->file_path.empty();
|
||||
}
|
||||
|
||||
const VideoMetadata& WebMFileReader::GetVideoMetadata() const {
|
||||
return m_state->metadata;
|
||||
}
|
||||
|
||||
std::string WebMFileReader::GetFilePath() const {
|
||||
return m_state ? m_state->file_path : "";
|
||||
}
|
||||
|
||||
std::vector<WebMFileReader::VideoTrackInfo> WebMFileReader::GetVideoTracks() const {
|
||||
return m_state ? m_state->video_tracks : std::vector<VideoTrackInfo>();
|
||||
}
|
||||
|
||||
bool WebMFileReader::SelectVideoTrack(uint64_t track_number) {
|
||||
if (!IsFileOpen()) {
|
||||
SetLastError(ErrorCode::ReadError, "File not open");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 트랙 존재 확인
|
||||
auto it = std::find_if(m_state->video_tracks.begin(), m_state->video_tracks.end(),
|
||||
[track_number](const VideoTrackInfo& info) {
|
||||
return info.track_number == track_number;
|
||||
});
|
||||
|
||||
if (it == m_state->video_tracks.end()) {
|
||||
SetLastError(ErrorCode::ReadError, "Track not found: " + std::to_string(track_number));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state->selected_track_number = track_number;
|
||||
Reset(); // 읽기 위치 초기화
|
||||
|
||||
SetLastError(ErrorCode::Success);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t WebMFileReader::GetSelectedTrackNumber() const {
|
||||
return m_state ? m_state->selected_track_number : 0;
|
||||
}
|
||||
|
||||
bool WebMFileReader::ReadNextPacket(VideoPacket& packet) {
|
||||
if (!IsFileOpen() || m_state->end_of_file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 선택된 트랙이 없으면 실패
|
||||
if (m_state->selected_track_number == 0) {
|
||||
SetLastError(ErrorCode::ReadError, "No video track selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 다음 블록 찾기
|
||||
if (!AdvanceToNextFrame()) {
|
||||
m_state->end_of_file = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 현재 블록에서 패킷 읽기
|
||||
if (!m_state->current_block_entry || !m_state->current_block_entry->GetBlock()) {
|
||||
SetLastError(ErrorCode::ReadError, "Invalid block entry");
|
||||
return false;
|
||||
}
|
||||
|
||||
const mkvparser::Block* block = m_state->current_block_entry->GetBlock();
|
||||
if (!ReadPacketFromBlock(block, packet)) {
|
||||
SetLastError(ErrorCode::ReadError, "Failed to read packet from block");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 현재 상태 업데이트
|
||||
m_state->current_frame_index++;
|
||||
|
||||
// 타임스탬프 계산
|
||||
const mkvparser::SegmentInfo* info = m_state->segment->GetInfo();
|
||||
if (info && m_state->current_cluster) {
|
||||
long long cluster_time = m_state->current_cluster->GetTime();
|
||||
long long block_time = block->GetTime(m_state->current_cluster);
|
||||
long long timecode_scale = info->GetTimeCodeScale();
|
||||
|
||||
m_state->current_timestamp = WebMUtils::TimecodeToSeconds(
|
||||
cluster_time + block_time, timecode_scale);
|
||||
|
||||
packet.timestamp_seconds = m_state->current_timestamp;
|
||||
}
|
||||
|
||||
packet.frame_index = m_state->current_frame_index - 1;
|
||||
packet.is_keyframe = block->IsKey();
|
||||
|
||||
SetLastError(ErrorCode::Success);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebMFileReader::SeekToFrame(uint64_t frame_index) {
|
||||
if (!IsFileOpen()) {
|
||||
SetLastError(ErrorCode::SeekError, "File not open");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_state->selected_track_number == 0) {
|
||||
SetLastError(ErrorCode::SeekError, "No video track selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 0번 프레임은 리셋과 동일
|
||||
if (frame_index == 0) {
|
||||
return Reset();
|
||||
}
|
||||
|
||||
// 프레임별 탐색은 시간 기반 탐색으로 변환
|
||||
// 프레임 레이트를 사용하여 대략적인 시간 계산
|
||||
double estimated_time = 0.0;
|
||||
if (m_state->metadata.frame_rate > 0) {
|
||||
estimated_time = static_cast<double>(frame_index) / m_state->metadata.frame_rate;
|
||||
}
|
||||
|
||||
// 시간 기반 탐색 후 정확한 프레임 찾기
|
||||
if (!SeekToTime(estimated_time)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 정확한 프레임 인덱스까지 순차 탐색
|
||||
while (m_state->current_frame_index < frame_index && !m_state->end_of_file) {
|
||||
if (!AdvanceToNextFrame()) {
|
||||
m_state->end_of_file = true;
|
||||
SetLastError(ErrorCode::SeekError, "Cannot reach target frame");
|
||||
return false;
|
||||
}
|
||||
m_state->current_frame_index++;
|
||||
}
|
||||
|
||||
SetLastError(ErrorCode::Success);
|
||||
return m_state->current_frame_index == frame_index;
|
||||
}
|
||||
|
||||
bool WebMFileReader::SeekToTime(double timestamp_seconds) {
|
||||
if (!IsFileOpen()) {
|
||||
SetLastError(ErrorCode::SeekError, "File not open");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_state->selected_track_number == 0) {
|
||||
SetLastError(ErrorCode::SeekError, "No video track selected");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (timestamp_seconds < 0.0) {
|
||||
SetLastError(ErrorCode::SeekError, "Invalid timestamp");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 시간이 0이면 리셋
|
||||
if (timestamp_seconds == 0.0) {
|
||||
return Reset();
|
||||
}
|
||||
|
||||
// 세그먼트 정보에서 타임코드 스케일 가져오기
|
||||
const mkvparser::SegmentInfo* info = m_state->segment->GetInfo();
|
||||
if (!info) {
|
||||
SetLastError(ErrorCode::SeekError, "No segment info available");
|
||||
return false;
|
||||
}
|
||||
|
||||
long long timecode_scale = info->GetTimeCodeScale();
|
||||
if (timecode_scale <= 0) {
|
||||
SetLastError(ErrorCode::SeekError, "Invalid timecode scale");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 타겟 타임코드 계산
|
||||
long long target_timecode = WebMUtils::SecondsToTimecode(timestamp_seconds, timecode_scale);
|
||||
|
||||
// 타겟 시간에 가장 가까운 클러스터 찾기
|
||||
const mkvparser::Cluster* target_cluster = FindClusterByTime(timestamp_seconds);
|
||||
if (!target_cluster) {
|
||||
SetLastError(ErrorCode::SeekError, "Cannot find target cluster");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
m_state->current_cluster = target_cluster;
|
||||
m_state->current_block_entry = nullptr;
|
||||
m_state->current_timestamp = timestamp_seconds;
|
||||
m_state->end_of_file = false;
|
||||
|
||||
// 정확한 시간의 프레임까지 이동
|
||||
while (!m_state->end_of_file) {
|
||||
if (!AdvanceToNextFrame()) {
|
||||
m_state->end_of_file = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// 현재 블록의 시간 확인
|
||||
if (m_state->current_block_entry && m_state->current_block_entry->GetBlock()) {
|
||||
const mkvparser::Block* block = m_state->current_block_entry->GetBlock();
|
||||
long long cluster_time = m_state->current_cluster->GetTime();
|
||||
long long block_time = block->GetTime(m_state->current_cluster);
|
||||
double current_time = WebMUtils::TimecodeToSeconds(cluster_time + block_time, timecode_scale);
|
||||
|
||||
if (current_time >= timestamp_seconds) {
|
||||
m_state->current_timestamp = current_time;
|
||||
SetLastError(ErrorCode::Success);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
m_state->current_frame_index++;
|
||||
}
|
||||
|
||||
SetLastError(ErrorCode::SeekError, "Cannot reach target time");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t WebMFileReader::GetCurrentFrameIndex() const {
|
||||
return m_state ? m_state->current_frame_index : 0;
|
||||
}
|
||||
|
||||
double WebMFileReader::GetCurrentTimestamp() const {
|
||||
return m_state ? m_state->current_timestamp : 0.0;
|
||||
}
|
||||
|
||||
bool WebMFileReader::IsEndOfFile() const {
|
||||
return m_state ? m_state->end_of_file : true;
|
||||
}
|
||||
|
||||
bool WebMFileReader::Reset() {
|
||||
if (!IsFileOpen()) return false;
|
||||
|
||||
// 첫 번째 클러스터로 이동
|
||||
const mkvparser::Cluster* cluster = m_state->segment->GetFirst();
|
||||
if (!cluster) {
|
||||
SetLastError(ErrorCode::ReadError, "No clusters found");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state->current_cluster = cluster;
|
||||
m_state->current_block_entry = nullptr;
|
||||
m_state->current_frame_index = 0;
|
||||
m_state->current_timestamp = 0.0;
|
||||
m_state->end_of_file = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t WebMFileReader::GetTotalFrames() const {
|
||||
return m_state ? m_state->metadata.total_frames : 0;
|
||||
}
|
||||
|
||||
double WebMFileReader::GetDuration() const {
|
||||
return m_state ? m_state->metadata.duration_seconds : 0.0;
|
||||
}
|
||||
|
||||
WebMFileReader::ErrorCode WebMFileReader::GetLastError() const {
|
||||
return m_state ? m_state->last_error : ErrorCode::Unknown;
|
||||
}
|
||||
|
||||
std::string WebMFileReader::GetLastErrorString() const {
|
||||
if (!m_state) return "Internal error";
|
||||
|
||||
std::string result = ErrorCodeToString(m_state->last_error);
|
||||
if (!m_state->last_error_message.empty()) {
|
||||
result += ": " + m_state->last_error_message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string WebMFileReader::GetLibWebMVersion() {
|
||||
int major, minor, build, revision;
|
||||
mkvparser::GetVersion(major, minor, build, revision);
|
||||
return std::to_string(major) + "." + std::to_string(minor) + "." +
|
||||
std::to_string(build) + "." + std::to_string(revision);
|
||||
}
|
||||
|
||||
std::vector<std::string> WebMFileReader::GetSupportedCodecs() {
|
||||
return { WebMUtils::CodecIds::AV1, WebMUtils::CodecIds::VP9, WebMUtils::CodecIds::VP8 };
|
||||
}
|
||||
|
||||
// 내부 helper 메서드 구현
|
||||
|
||||
bool WebMFileReader::InitializeParser() {
|
||||
long long pos = 0;
|
||||
mkvparser::EBMLHeader ebml_header;
|
||||
|
||||
// EBML 헤더 파싱
|
||||
long long ebml_header_size = ebml_header.Parse(m_state->reader.get(), pos);
|
||||
|
||||
if (ebml_header_size < 0) {
|
||||
std::string detailed_error = "Invalid EBML header - Parse() returned: " + std::to_string(ebml_header_size);
|
||||
|
||||
// 추가 디버깅: 파일 시작 부분 읽기 (올바른 Read 메서드 시그니처 사용)
|
||||
char buffer[32];
|
||||
int read_result = m_state->reader->Read(0, 32, reinterpret_cast<unsigned char*>(buffer));
|
||||
if (read_result == 0) {
|
||||
std::string hex_dump = ". First 32 bytes: ";
|
||||
for (int i = 0; i < 32; i++) {
|
||||
char hex[4];
|
||||
sprintf_s(hex, "%02X ", static_cast<unsigned char>(buffer[i]));
|
||||
hex_dump += hex;
|
||||
}
|
||||
detailed_error += hex_dump;
|
||||
}
|
||||
|
||||
SetLastError(ErrorCode::InvalidFormat, detailed_error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// WebM 파일 확인
|
||||
if (!ebml_header.m_docType || std::string(ebml_header.m_docType) != "webm") {
|
||||
std::string doc_type = ebml_header.m_docType ? std::string(ebml_header.m_docType) : "NULL";
|
||||
std::string error_msg = "Not a WebM file - doc type is: '" + doc_type + "'";
|
||||
|
||||
// 추가 디버깅 정보
|
||||
error_msg += ". EBML header details: version=" + std::to_string(ebml_header.m_version) +
|
||||
", docTypeVersion=" + std::to_string(ebml_header.m_docTypeVersion);
|
||||
|
||||
SetLastError(ErrorCode::InvalidFormat, error_msg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Segment 파싱
|
||||
mkvparser::Segment* segment_ptr = nullptr;
|
||||
long long create_status = mkvparser::Segment::CreateInstance(
|
||||
m_state->reader.get(), pos, segment_ptr);
|
||||
|
||||
if (create_status < 0 || !segment_ptr) {
|
||||
SetLastError(ErrorCode::InvalidFormat, "Cannot create segment");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_state->segment.reset(segment_ptr);
|
||||
|
||||
if (!m_state->segment) {
|
||||
SetLastError(ErrorCode::InvalidFormat, "Cannot create segment");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Segment 정보 로드
|
||||
long load_status = m_state->segment->Load();
|
||||
if (load_status < 0) {
|
||||
SetLastError(ErrorCode::InvalidFormat, "Cannot load segment");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WebMFileReader::CleanupParser() {
|
||||
if (m_state) {
|
||||
m_state->segment.reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool WebMFileReader::ExtractVideoTracks() {
|
||||
if (!m_state->segment) return false;
|
||||
|
||||
m_state->video_tracks.clear();
|
||||
|
||||
const mkvparser::Tracks* tracks = m_state->segment->GetTracks();
|
||||
if (!tracks) return false;
|
||||
|
||||
for (unsigned long i = 0; i < tracks->GetTracksCount(); ++i) {
|
||||
const mkvparser::Track* track = tracks->GetTrackByIndex(i);
|
||||
if (!track || track->GetType() != mkvparser::Track::kVideo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mkvparser::VideoTrack* video_track =
|
||||
static_cast<const mkvparser::VideoTrack*>(track);
|
||||
|
||||
VideoTrackInfo info;
|
||||
info.track_number = video_track->GetNumber();
|
||||
info.codec_id = video_track->GetCodecId() ? video_track->GetCodecId() : "";
|
||||
|
||||
// 디버깅: 실제 코덱 ID 로그 출력
|
||||
std::string debug_msg = "Found video track #" + std::to_string(info.track_number) +
|
||||
" with codec_id: '" + info.codec_id + "'";
|
||||
SetLastError(ErrorCode::Success, debug_msg); // 임시로 이 메시지를 상태에 표시
|
||||
|
||||
info.codec_type = DetectCodecType(info.codec_id);
|
||||
info.codec_name = ExtractCodecName(info.codec_id);
|
||||
info.width = static_cast<uint32_t>(video_track->GetWidth());
|
||||
info.height = static_cast<uint32_t>(video_track->GetHeight());
|
||||
info.frame_rate = CalculateFrameRate(video_track);
|
||||
info.frame_count = 0; // TODO: 실제 계산
|
||||
info.is_default = false; // TODO: 트랙의 기본 설정 확인
|
||||
|
||||
m_state->video_tracks.push_back(info);
|
||||
}
|
||||
|
||||
return !m_state->video_tracks.empty();
|
||||
}
|
||||
|
||||
bool WebMFileReader::ExtractVideoMetadata() {
|
||||
if (m_state->selected_track_number == 0) return false;
|
||||
|
||||
// 선택된 트랙 찾기
|
||||
auto it = std::find_if(m_state->video_tracks.begin(), m_state->video_tracks.end(),
|
||||
[this](const VideoTrackInfo& info) {
|
||||
return info.track_number == m_state->selected_track_number;
|
||||
});
|
||||
|
||||
if (it == m_state->video_tracks.end()) return false;
|
||||
|
||||
// 메타데이터 설정
|
||||
VideoMetadata& meta = m_state->metadata;
|
||||
meta.width = it->width;
|
||||
meta.height = it->height;
|
||||
meta.frame_rate = it->frame_rate;
|
||||
meta.codec_type = it->codec_type;
|
||||
meta.codec_name = it->codec_name;
|
||||
meta.color_space = ColorSpace::YUV420P; // 기본값
|
||||
|
||||
// 세그먼트 정보에서 duration 가져오기
|
||||
const mkvparser::SegmentInfo* info = m_state->segment->GetInfo();
|
||||
if (info) {
|
||||
long long duration_ns = info->GetDuration();
|
||||
long long timecode_scale = info->GetTimeCodeScale();
|
||||
if (duration_ns > 0 && timecode_scale > 0) {
|
||||
meta.duration_seconds = static_cast<double>(duration_ns) / timecode_scale / 1000000000.0;
|
||||
}
|
||||
}
|
||||
|
||||
// 총 프레임 수 추정
|
||||
if (meta.frame_rate > 0 && meta.duration_seconds > 0) {
|
||||
meta.total_frames = static_cast<uint64_t>(meta.frame_rate * meta.duration_seconds);
|
||||
}
|
||||
|
||||
meta.file_path = m_state->file_path;
|
||||
|
||||
return meta.IsValid();
|
||||
}
|
||||
|
||||
VideoCodecType WebMFileReader::DetectCodecType(const std::string& codec_id) const {
|
||||
if (codec_id == WebMUtils::CodecIds::AV1) return VideoCodecType::AV1;
|
||||
if (codec_id == WebMUtils::CodecIds::VP9) return VideoCodecType::VP9;
|
||||
if (codec_id == WebMUtils::CodecIds::VP8) return VideoCodecType::VP8;
|
||||
return VideoCodecType::AV1; // 기본값
|
||||
}
|
||||
|
||||
double WebMFileReader::CalculateFrameRate(const mkvparser::VideoTrack* video_track) const {
|
||||
if (!video_track) return 0.0;
|
||||
|
||||
double frame_rate = video_track->GetFrameRate();
|
||||
if (frame_rate > 0) return frame_rate;
|
||||
|
||||
// TODO: 클러스터 분석으로 프레임 레이트 추정
|
||||
return 30.0; // 임시 기본값
|
||||
}
|
||||
|
||||
void WebMFileReader::SetLastError(ErrorCode error, const std::string& message) {
|
||||
if (m_state) {
|
||||
m_state->last_error = error;
|
||||
m_state->last_error_message = message;
|
||||
}
|
||||
}
|
||||
|
||||
std::string WebMFileReader::ErrorCodeToString(ErrorCode error) const {
|
||||
switch (error) {
|
||||
case ErrorCode::Success: return "Success";
|
||||
case ErrorCode::FileNotFound: return "File not found";
|
||||
case ErrorCode::InvalidFormat: return "Invalid format";
|
||||
case ErrorCode::UnsupportedCodec: return "Unsupported codec";
|
||||
case ErrorCode::NoVideoTrack: return "No video track";
|
||||
case ErrorCode::ReadError: return "Read error";
|
||||
case ErrorCode::SeekError: return "Seek error";
|
||||
default: return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
bool WebMFileReader::IsVideoCodecSupported(const std::string& codec_id) {
|
||||
return codec_id == WebMUtils::CodecIds::AV1 ||
|
||||
codec_id == WebMUtils::CodecIds::VP9; // VP8은 현재 미지원
|
||||
}
|
||||
|
||||
std::string WebMFileReader::ExtractCodecName(const std::string& codec_id) {
|
||||
if (codec_id == WebMUtils::CodecIds::AV1) return "AV1";
|
||||
if (codec_id == WebMUtils::CodecIds::VP9) return "VP9";
|
||||
if (codec_id == WebMUtils::CodecIds::VP8) return "VP8";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
bool WebMFileReader::ReadPacketFromBlock(const mkvparser::Block* block, VideoPacket& packet) {
|
||||
if (!block) return false;
|
||||
|
||||
// 선택된 트랙과 블록의 트랙 번호 확인
|
||||
if (static_cast<uint64_t>(block->GetTrackNumber()) != m_state->selected_track_number) {
|
||||
return false; // 다른 트랙의 블록이므로 스킵
|
||||
}
|
||||
|
||||
// 블록에서 프레임 수 확인 (일반적으로 1개)
|
||||
int frame_count = block->GetFrameCount();
|
||||
if (frame_count <= 0) return false;
|
||||
|
||||
// 첫 번째 프레임 데이터 가져오기
|
||||
const mkvparser::Block::Frame& frame = block->GetFrame(0);
|
||||
if (frame.len <= 0) return false;
|
||||
|
||||
// VideoPacket 메모리 할당
|
||||
if (!packet.AllocateData(static_cast<size_t>(frame.len))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 프레임 데이터 읽기
|
||||
if (frame.Read(m_state->reader.get(), packet.data.get()) < 0) {
|
||||
packet.data.reset();
|
||||
packet.size = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WebMFileReader::AdvanceToNextFrame() {
|
||||
if (!m_state->current_cluster) {
|
||||
// 첫 번째 클러스터로 시작
|
||||
m_state->current_cluster = m_state->segment->GetFirst();
|
||||
if (!m_state->current_cluster) return false;
|
||||
}
|
||||
|
||||
// 현재 클러스터에서 다음 블록 엔트리 찾기
|
||||
while (m_state->current_cluster) {
|
||||
const mkvparser::BlockEntry* block_entry = nullptr;
|
||||
|
||||
if (m_state->current_block_entry) {
|
||||
// 다음 블록 엔트리로 이동
|
||||
long status = m_state->current_cluster->GetNext(m_state->current_block_entry, block_entry);
|
||||
if (status < 0) return false;
|
||||
} else {
|
||||
// 클러스터의 첫 번째 블록 엔트리 가져오기
|
||||
long status = m_state->current_cluster->GetFirst(block_entry);
|
||||
if (status < 0) return false;
|
||||
}
|
||||
|
||||
// 블록 엔트리가 있으면서 선택된 트랙의 블록인지 확인
|
||||
while (block_entry && !block_entry->EOS()) {
|
||||
const mkvparser::Block* block = block_entry->GetBlock();
|
||||
if (block && static_cast<uint64_t>(block->GetTrackNumber()) == m_state->selected_track_number) {
|
||||
m_state->current_block_entry = block_entry;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 다른 트랙의 블록이므로 다음 블록으로 이동
|
||||
const mkvparser::BlockEntry* next_entry = nullptr;
|
||||
long status = m_state->current_cluster->GetNext(block_entry, next_entry);
|
||||
if (status < 0) break;
|
||||
block_entry = next_entry;
|
||||
}
|
||||
|
||||
// 현재 클러스터에서 더 이상 블록이 없으면 다음 클러스터로 이동
|
||||
m_state->current_cluster = m_state->segment->GetNext(m_state->current_cluster);
|
||||
m_state->current_block_entry = nullptr;
|
||||
}
|
||||
|
||||
// 더 이상 클러스터가 없음
|
||||
return false;
|
||||
}
|
||||
|
||||
const mkvparser::Cluster* WebMFileReader::FindClusterByTime(double timestamp_seconds) {
|
||||
if (!m_state->segment) return nullptr;
|
||||
|
||||
const mkvparser::SegmentInfo* info = m_state->segment->GetInfo();
|
||||
if (!info) return nullptr;
|
||||
|
||||
long long timecode_scale = info->GetTimeCodeScale();
|
||||
if (timecode_scale <= 0) return nullptr;
|
||||
|
||||
// 타겟 타임코드 계산
|
||||
long long target_timecode = WebMUtils::SecondsToTimecode(timestamp_seconds, timecode_scale);
|
||||
|
||||
// 첫 번째 클러스터부터 순차 탐색
|
||||
const mkvparser::Cluster* cluster = m_state->segment->GetFirst();
|
||||
const mkvparser::Cluster* best_cluster = cluster;
|
||||
|
||||
while (cluster && !cluster->EOS()) {
|
||||
long long cluster_time = cluster->GetTime();
|
||||
|
||||
// 타겟 시간에 가장 가까운 클러스터 찾기
|
||||
if (cluster_time <= target_timecode) {
|
||||
best_cluster = cluster;
|
||||
} else {
|
||||
break; // 타겟 시간을 넘어섰으므로 이전 클러스터가 최적
|
||||
}
|
||||
|
||||
cluster = m_state->segment->GetNext(cluster);
|
||||
}
|
||||
|
||||
return best_cluster;
|
||||
}
|
||||
|
||||
// WebMUtils 구현
|
||||
namespace WebMUtils {
|
||||
|
||||
bool IsWebMFile(const std::string& file_path) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) return false;
|
||||
|
||||
uint8_t header[32];
|
||||
file.read(reinterpret_cast<char*>(header), sizeof(header));
|
||||
|
||||
return IsValidWebMHeader(header, file.gcount());
|
||||
}
|
||||
|
||||
bool IsValidWebMHeader(const uint8_t* data, size_t size) {
|
||||
if (!data || size < 4) return false;
|
||||
|
||||
// EBML 헤더 시그니처 확인 (0x1A45DFA3)
|
||||
return data[0] == 0x1A && data[1] == 0x45 &&
|
||||
data[2] == 0xDF && data[3] == 0xA3;
|
||||
}
|
||||
|
||||
std::vector<CodecInfo> GetAllWebMCodecs() {
|
||||
return {
|
||||
{ CodecIds::AV1, "AV1", VideoCodecType::AV1, true },
|
||||
{ CodecIds::VP9, "VP9", VideoCodecType::VP9, false }, // TODO: VP9 지원
|
||||
{ CodecIds::VP8, "VP8", VideoCodecType::VP8, false }
|
||||
};
|
||||
}
|
||||
|
||||
CodecInfo GetCodecInfo(const std::string& codec_id) {
|
||||
auto codecs = GetAllWebMCodecs();
|
||||
auto it = std::find_if(codecs.begin(), codecs.end(),
|
||||
[&codec_id](const CodecInfo& info) {
|
||||
return info.codec_id == codec_id;
|
||||
});
|
||||
|
||||
if (it != codecs.end()) {
|
||||
return *it;
|
||||
}
|
||||
|
||||
return { codec_id, "Unknown", VideoCodecType::AV1, false };
|
||||
}
|
||||
|
||||
double TimecodeToSeconds(long long timecode, long long timecode_scale) {
|
||||
if (timecode_scale <= 0) return 0.0;
|
||||
return static_cast<double>(timecode) * timecode_scale / 1000000000.0;
|
||||
}
|
||||
|
||||
long long SecondsToTimecode(double seconds, long long timecode_scale) {
|
||||
if (timecode_scale <= 0) return 0;
|
||||
return static_cast<long long>(seconds * 1000000000.0 / timecode_scale);
|
||||
}
|
||||
|
||||
} // namespace WebMUtils
|
||||
|
||||
} // namespace Vav2Player
|
||||
160
vav2/Vav2Player/Vav2Player/src/FileIO/WebMFileReader.h
Normal file
@@ -0,0 +1,160 @@
|
||||
#pragma once
|
||||
#include "../Common/VideoTypes.h"
|
||||
#include <mkvparser.hpp>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// WebM/MKV 파일을 파싱하여 AV1 비디오 스트림을 추출하는 클래스
|
||||
// libwebm의 mkvparser를 사용하여 구현
|
||||
class WebMFileReader {
|
||||
public:
|
||||
WebMFileReader();
|
||||
~WebMFileReader();
|
||||
|
||||
// 복사 방지
|
||||
WebMFileReader(const WebMFileReader&) = delete;
|
||||
WebMFileReader& operator=(const WebMFileReader&) = delete;
|
||||
|
||||
// 파일 열기/닫기
|
||||
bool OpenFile(const std::string& file_path);
|
||||
void CloseFile();
|
||||
bool IsFileOpen() const;
|
||||
|
||||
// 파일 및 스트림 정보
|
||||
const VideoMetadata& GetVideoMetadata() const;
|
||||
std::string GetFilePath() const;
|
||||
|
||||
// 지원되는 비디오 트랙들 (AV1, VP9 등)
|
||||
struct VideoTrackInfo {
|
||||
uint64_t track_number;
|
||||
VideoCodecType codec_type;
|
||||
std::string codec_id;
|
||||
std::string codec_name;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
double frame_rate;
|
||||
uint64_t frame_count;
|
||||
bool is_default;
|
||||
};
|
||||
|
||||
std::vector<VideoTrackInfo> GetVideoTracks() const;
|
||||
bool SelectVideoTrack(uint64_t track_number);
|
||||
uint64_t GetSelectedTrackNumber() const;
|
||||
|
||||
// 프레임별 패킷 읽기
|
||||
bool ReadNextPacket(VideoPacket& packet);
|
||||
bool SeekToFrame(uint64_t frame_index);
|
||||
bool SeekToTime(double timestamp_seconds);
|
||||
|
||||
// 현재 위치 정보
|
||||
uint64_t GetCurrentFrameIndex() const;
|
||||
double GetCurrentTimestamp() const;
|
||||
bool IsEndOfFile() const;
|
||||
|
||||
// 파일 탐색 및 통계
|
||||
bool Reset(); // 처음으로 되돌아가기
|
||||
uint64_t GetTotalFrames() const;
|
||||
double GetDuration() const;
|
||||
|
||||
// 에러 처리
|
||||
enum class ErrorCode {
|
||||
Success,
|
||||
FileNotFound,
|
||||
InvalidFormat,
|
||||
UnsupportedCodec,
|
||||
NoVideoTrack,
|
||||
ReadError,
|
||||
SeekError,
|
||||
Unknown
|
||||
};
|
||||
|
||||
ErrorCode GetLastError() const;
|
||||
std::string GetLastErrorString() const;
|
||||
|
||||
// libwebm 관련 정보
|
||||
static std::string GetLibWebMVersion();
|
||||
static std::vector<std::string> GetSupportedCodecs();
|
||||
|
||||
private:
|
||||
// libwebm 관련 내부 클래스들
|
||||
class MkvReader; // mkvparser::IMkvReader 구현체
|
||||
|
||||
// 내부 상태 관리
|
||||
struct InternalState;
|
||||
std::unique_ptr<InternalState> m_state;
|
||||
|
||||
// 내부 helper 메서드들
|
||||
bool InitializeParser();
|
||||
void CleanupParser();
|
||||
bool ParseFileHeader();
|
||||
bool ExtractVideoTracks();
|
||||
bool ValidateSelectedTrack();
|
||||
|
||||
// 메타데이터 추출
|
||||
bool ExtractVideoMetadata();
|
||||
VideoCodecType DetectCodecType(const std::string& codec_id) const;
|
||||
double CalculateFrameRate(const mkvparser::VideoTrack* video_track) const;
|
||||
uint64_t EstimateFrameCount() const;
|
||||
|
||||
// 패킷 읽기 관련
|
||||
bool ReadPacketFromBlock(const mkvparser::Block* block, VideoPacket& packet);
|
||||
bool AdvanceToNextFrame();
|
||||
|
||||
// 탐색 관련
|
||||
const mkvparser::Cluster* FindClusterByTime(double timestamp_seconds);
|
||||
const mkvparser::Block* FindBlockByFrame(uint64_t frame_index);
|
||||
|
||||
// 에러 처리
|
||||
void SetLastError(ErrorCode error, const std::string& message = "");
|
||||
std::string ErrorCodeToString(ErrorCode error) const;
|
||||
|
||||
// 유틸리티 함수들
|
||||
static bool IsVideoCodecSupported(const std::string& codec_id);
|
||||
static std::string ExtractCodecName(const std::string& codec_id);
|
||||
static ColorSpace GetColorSpaceFromTrack(const mkvparser::VideoTrack* track);
|
||||
};
|
||||
|
||||
// WebM 관련 유틸리티 함수들
|
||||
namespace WebMUtils {
|
||||
// WebM 파일 검증
|
||||
bool IsWebMFile(const std::string& file_path);
|
||||
bool IsValidWebMHeader(const uint8_t* data, size_t size);
|
||||
|
||||
// 코덱 정보
|
||||
struct CodecInfo {
|
||||
std::string codec_id;
|
||||
std::string friendly_name;
|
||||
VideoCodecType codec_type;
|
||||
bool is_supported;
|
||||
};
|
||||
|
||||
std::vector<CodecInfo> GetAllWebMCodecs();
|
||||
CodecInfo GetCodecInfo(const std::string& codec_id);
|
||||
|
||||
// WebM 포맷 상수들
|
||||
namespace CodecIds {
|
||||
constexpr const char* AV1 = "V_AV01";
|
||||
constexpr const char* VP9 = "V_VP9";
|
||||
constexpr const char* VP8 = "V_VP8";
|
||||
}
|
||||
|
||||
// 시간/타임스탬프 변환
|
||||
double TimecodeToSeconds(long long timecode, long long timecode_scale);
|
||||
long long SecondsToTimecode(double seconds, long long timecode_scale);
|
||||
|
||||
// 파일 크기 및 정보
|
||||
struct FileInfo {
|
||||
uint64_t file_size;
|
||||
std::string file_format;
|
||||
std::string muxing_app;
|
||||
std::string writing_app;
|
||||
double duration_seconds;
|
||||
};
|
||||
|
||||
FileInfo GetWebMFileInfo(const std::string& file_path);
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
446
vav2/Vav2Player/Vav2Player/src/Output/FileOutput.cpp
Normal file
@@ -0,0 +1,446 @@
|
||||
#include "pch.h"
|
||||
#include "FileOutput.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <mutex>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
FileOutput::FileOutput(const OutputConfig& config)
|
||||
: m_config(config) {
|
||||
m_stats.start_time = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
void FileOutput::SetConfig(const OutputConfig& config) {
|
||||
m_config = config;
|
||||
}
|
||||
|
||||
void FileOutput::SetProgressCallback(ProgressCallback callback) {
|
||||
m_progress_callback = std::move(callback);
|
||||
}
|
||||
|
||||
FileOutput::SaveResult FileOutput::SaveFrame(const VideoFrame& frame, uint64_t frame_index, double timestamp) {
|
||||
auto start_time = std::chrono::high_resolution_clock::now();
|
||||
|
||||
// 출력 디렉토리 생성
|
||||
if (!CreateOutputDirectory()) {
|
||||
return CreateErrorResult("Failed to create output directory");
|
||||
}
|
||||
|
||||
// 파일명 생성
|
||||
auto filename = GenerateFilename(m_config.filename_prefix, frame_index, m_config.format);
|
||||
auto file_path = m_config.output_directory / filename;
|
||||
|
||||
// 기존 파일 덮어쓰기 확인
|
||||
if (!m_config.overwrite_existing && std::filesystem::exists(file_path)) {
|
||||
return CreateErrorResult("File already exists and overwrite is disabled");
|
||||
}
|
||||
|
||||
SaveResult result;
|
||||
|
||||
// 포맷별 저장
|
||||
switch (m_config.format) {
|
||||
case OutputFormat::RawYUV:
|
||||
result = SaveAsRawYUV(frame, file_path);
|
||||
break;
|
||||
case OutputFormat::BMP:
|
||||
result = SaveAsBMP(frame, file_path);
|
||||
break;
|
||||
case OutputFormat::PNG:
|
||||
result = CreateErrorResult("PNG format not implemented yet");
|
||||
break;
|
||||
default:
|
||||
result = CreateErrorResult("Unknown output format");
|
||||
break;
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
auto end_time = std::chrono::high_resolution_clock::now();
|
||||
auto duration_ms = std::chrono::duration<double, std::milli>(end_time - start_time).count();
|
||||
UpdateSaveStats(result.success, result.file_size_bytes, duration_ms);
|
||||
|
||||
// 진행 상황 콜백 호출
|
||||
if (result.success && m_progress_callback) {
|
||||
m_progress_callback(frame_index, result.saved_path);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool FileOutput::SaveFrameSequence(const VideoFrame& frame, uint64_t frame_index, double timestamp) {
|
||||
auto result = SaveFrame(frame, frame_index, timestamp);
|
||||
return result.success;
|
||||
}
|
||||
|
||||
bool FileOutput::CreateOutputDirectory() {
|
||||
try {
|
||||
if (!std::filesystem::exists(m_config.output_directory)) {
|
||||
return std::filesystem::create_directories(m_config.output_directory);
|
||||
}
|
||||
return true;
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "[FileOutput Error] Failed to create directory: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void FileOutput::ClearOutputDirectory() {
|
||||
try {
|
||||
if (std::filesystem::exists(m_config.output_directory)) {
|
||||
for (const auto& entry : std::filesystem::directory_iterator(m_config.output_directory)) {
|
||||
std::filesystem::remove(entry);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "[FileOutput Error] Failed to clear directory: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
FileOutput::OutputStats FileOutput::GetStats() const {
|
||||
std::lock_guard<std::mutex> lock(m_stats_mutex);
|
||||
return m_stats;
|
||||
}
|
||||
|
||||
void FileOutput::ResetStats() {
|
||||
std::lock_guard<std::mutex> lock(m_stats_mutex);
|
||||
m_stats = OutputStats{};
|
||||
m_stats.start_time = std::chrono::high_resolution_clock::now();
|
||||
}
|
||||
|
||||
std::string FileOutput::FormatToString(OutputFormat format) {
|
||||
switch (format) {
|
||||
case OutputFormat::RawYUV: return "Raw YUV";
|
||||
case OutputFormat::BMP: return "BMP";
|
||||
case OutputFormat::PNG: return "PNG";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path FileOutput::GenerateFilename(const std::string& prefix,
|
||||
uint64_t frame_index,
|
||||
OutputFormat format) {
|
||||
std::string extension;
|
||||
switch (format) {
|
||||
case OutputFormat::RawYUV: extension = ".yuv"; break;
|
||||
case OutputFormat::BMP: extension = ".bmp"; break;
|
||||
case OutputFormat::PNG: extension = ".png"; break;
|
||||
default: extension = ".dat"; break;
|
||||
}
|
||||
|
||||
return std::filesystem::path(prefix + "_" + std::to_string(frame_index) + extension);
|
||||
}
|
||||
|
||||
// Raw YUV 파일 저장
|
||||
FileOutput::SaveResult FileOutput::SaveAsRawYUV(const VideoFrame& frame, const std::filesystem::path& file_path) {
|
||||
try {
|
||||
std::ofstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return CreateErrorResult("Failed to open file for writing");
|
||||
}
|
||||
|
||||
size_t total_bytes = 0;
|
||||
|
||||
// Y 플레인 저장
|
||||
for (uint32_t y = 0; y < frame.height; ++y) {
|
||||
const uint8_t* row_data = frame.y_plane.get() + (y * frame.y_stride);
|
||||
file.write(reinterpret_cast<const char*>(row_data), frame.width);
|
||||
total_bytes += frame.width;
|
||||
}
|
||||
|
||||
// U 플레인 저장
|
||||
uint32_t chroma_width = (frame.color_space == ColorSpace::YUV444P) ? frame.width : frame.width / 2;
|
||||
uint32_t chroma_height = (frame.color_space == ColorSpace::YUV420P) ? frame.height / 2 : frame.height;
|
||||
|
||||
for (uint32_t y = 0; y < chroma_height; ++y) {
|
||||
const uint8_t* row_data = frame.u_plane.get() + (y * frame.u_stride);
|
||||
file.write(reinterpret_cast<const char*>(row_data), chroma_width);
|
||||
total_bytes += chroma_width;
|
||||
}
|
||||
|
||||
// V 플레인 저장
|
||||
for (uint32_t y = 0; y < chroma_height; ++y) {
|
||||
const uint8_t* row_data = frame.v_plane.get() + (y * frame.v_stride);
|
||||
file.write(reinterpret_cast<const char*>(row_data), chroma_width);
|
||||
total_bytes += chroma_width;
|
||||
}
|
||||
|
||||
SaveResult result;
|
||||
result.success = true;
|
||||
result.saved_path = file_path;
|
||||
result.file_size_bytes = total_bytes;
|
||||
return result;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
return CreateErrorResult("Exception during YUV save: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
// BMP 파일 저장
|
||||
FileOutput::SaveResult FileOutput::SaveAsBMP(const VideoFrame& frame, const std::filesystem::path& file_path) {
|
||||
// YUV → RGB 변환
|
||||
RGBFrame rgb_frame;
|
||||
if (!ConvertYUVToRGB(frame, rgb_frame)) {
|
||||
return CreateErrorResult("Failed to convert YUV to RGB");
|
||||
}
|
||||
|
||||
try {
|
||||
std::ofstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return CreateErrorResult("Failed to open BMP file for writing");
|
||||
}
|
||||
|
||||
// BMP 헤더 생성
|
||||
auto header = CreateBMPHeader(rgb_frame.width, rgb_frame.height);
|
||||
|
||||
// 파일 헤더 쓰기
|
||||
file.write(reinterpret_cast<const char*>(&header.file_type), sizeof(header.file_type));
|
||||
file.write(reinterpret_cast<const char*>(&header.file_size), sizeof(header.file_size));
|
||||
file.write(reinterpret_cast<const char*>(&header.reserved1), sizeof(header.reserved1));
|
||||
file.write(reinterpret_cast<const char*>(&header.reserved2), sizeof(header.reserved2));
|
||||
file.write(reinterpret_cast<const char*>(&header.offset_data), sizeof(header.offset_data));
|
||||
|
||||
// 정보 헤더 쓰기
|
||||
file.write(reinterpret_cast<const char*>(&header.size), sizeof(header.size));
|
||||
file.write(reinterpret_cast<const char*>(&header.width), sizeof(header.width));
|
||||
file.write(reinterpret_cast<const char*>(&header.height), sizeof(header.height));
|
||||
file.write(reinterpret_cast<const char*>(&header.planes), sizeof(header.planes));
|
||||
file.write(reinterpret_cast<const char*>(&header.bit_count), sizeof(header.bit_count));
|
||||
file.write(reinterpret_cast<const char*>(&header.compression), sizeof(header.compression));
|
||||
file.write(reinterpret_cast<const char*>(&header.size_image), sizeof(header.size_image));
|
||||
file.write(reinterpret_cast<const char*>(&header.x_pixels_per_meter), sizeof(header.x_pixels_per_meter));
|
||||
file.write(reinterpret_cast<const char*>(&header.y_pixels_per_meter), sizeof(header.y_pixels_per_meter));
|
||||
file.write(reinterpret_cast<const char*>(&header.colors_used), sizeof(header.colors_used));
|
||||
file.write(reinterpret_cast<const char*>(&header.colors_important), sizeof(header.colors_important));
|
||||
|
||||
// RGB 데이터 쓰기 (BMP는 bottom-up이므로 행을 뒤집어서 저장)
|
||||
uint32_t row_size = rgb_frame.width * 3;
|
||||
uint32_t padding = (4 - (row_size % 4)) % 4; // 4바이트 정렬
|
||||
uint8_t padding_bytes[3] = {0, 0, 0};
|
||||
|
||||
for (int32_t y = static_cast<int32_t>(rgb_frame.height) - 1; y >= 0; --y) {
|
||||
const uint8_t* row_data = rgb_frame.data.data() + (y * rgb_frame.stride);
|
||||
|
||||
// BGR 순서로 변환하여 저장 (BMP는 BGR)
|
||||
for (uint32_t x = 0; x < rgb_frame.width; ++x) {
|
||||
uint8_t r = row_data[x * 3 + 0];
|
||||
uint8_t g = row_data[x * 3 + 1];
|
||||
uint8_t b = row_data[x * 3 + 2];
|
||||
|
||||
file.write(reinterpret_cast<const char*>(&b), 1);
|
||||
file.write(reinterpret_cast<const char*>(&g), 1);
|
||||
file.write(reinterpret_cast<const char*>(&r), 1);
|
||||
}
|
||||
|
||||
// 패딩 추가
|
||||
if (padding > 0) {
|
||||
file.write(reinterpret_cast<const char*>(padding_bytes), padding);
|
||||
}
|
||||
}
|
||||
|
||||
SaveResult result;
|
||||
result.success = true;
|
||||
result.saved_path = file_path;
|
||||
result.file_size_bytes = static_cast<size_t>(file.tellp());
|
||||
return result;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
return CreateErrorResult("Exception during BMP save: " + std::string(e.what()));
|
||||
}
|
||||
}
|
||||
|
||||
// YUV → RGB 변환
|
||||
bool FileOutput::ConvertYUVToRGB(const VideoFrame& yuv_frame, RGBFrame& rgb_frame) {
|
||||
rgb_frame.width = yuv_frame.width;
|
||||
rgb_frame.height = yuv_frame.height;
|
||||
rgb_frame.stride = yuv_frame.width * 3; // RGB24
|
||||
rgb_frame.data.resize(rgb_frame.height * rgb_frame.stride);
|
||||
|
||||
switch (yuv_frame.color_space) {
|
||||
case ColorSpace::YUV420P:
|
||||
ConvertYUV420PToRGB24(yuv_frame.y_plane.get(), yuv_frame.u_plane.get(), yuv_frame.v_plane.get(),
|
||||
yuv_frame.width, yuv_frame.height,
|
||||
yuv_frame.y_stride, yuv_frame.u_stride, yuv_frame.v_stride,
|
||||
rgb_frame.data.data(), rgb_frame.stride);
|
||||
break;
|
||||
case ColorSpace::YUV422P:
|
||||
ConvertYUV422PToRGB24(yuv_frame.y_plane.get(), yuv_frame.u_plane.get(), yuv_frame.v_plane.get(),
|
||||
yuv_frame.width, yuv_frame.height,
|
||||
yuv_frame.y_stride, yuv_frame.u_stride, yuv_frame.v_stride,
|
||||
rgb_frame.data.data(), rgb_frame.stride);
|
||||
break;
|
||||
case ColorSpace::YUV444P:
|
||||
ConvertYUV444PToRGB24(yuv_frame.y_plane.get(), yuv_frame.u_plane.get(), yuv_frame.v_plane.get(),
|
||||
yuv_frame.width, yuv_frame.height,
|
||||
yuv_frame.y_stride, yuv_frame.u_stride, yuv_frame.v_stride,
|
||||
rgb_frame.data.data(), rgb_frame.stride);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// YUV420P → RGB24 변환 (4:2:0 서브샘플링)
|
||||
void FileOutput::ConvertYUV420PToRGB24(const uint8_t* y_plane, const uint8_t* u_plane, const uint8_t* v_plane,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t y_stride, uint32_t u_stride, uint32_t v_stride,
|
||||
uint8_t* rgb_data, uint32_t rgb_stride) {
|
||||
for (uint32_t y = 0; y < height; ++y) {
|
||||
for (uint32_t x = 0; x < width; ++x) {
|
||||
// YUV 값 가져오기
|
||||
uint8_t Y = y_plane[y * y_stride + x];
|
||||
uint8_t U = u_plane[(y / 2) * u_stride + (x / 2)];
|
||||
uint8_t V = v_plane[(y / 2) * v_stride + (x / 2)];
|
||||
|
||||
// YUV → RGB 변환 (BT.601 표준)
|
||||
int C = Y - 16;
|
||||
int D = U - 128;
|
||||
int E = V - 128;
|
||||
|
||||
int R = (298 * C + 409 * E + 128) >> 8;
|
||||
int G = (298 * C - 100 * D - 208 * E + 128) >> 8;
|
||||
int B = (298 * C + 516 * D + 128) >> 8;
|
||||
|
||||
// 클램핑 (0-255)
|
||||
R = std::clamp(R, 0, 255);
|
||||
G = std::clamp(G, 0, 255);
|
||||
B = std::clamp(B, 0, 255);
|
||||
|
||||
// RGB 데이터 저장
|
||||
uint8_t* pixel = rgb_data + (y * rgb_stride) + (x * 3);
|
||||
pixel[0] = static_cast<uint8_t>(R);
|
||||
pixel[1] = static_cast<uint8_t>(G);
|
||||
pixel[2] = static_cast<uint8_t>(B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// YUV422P → RGB24 변환 (4:2:2 서브샘플링)
|
||||
void FileOutput::ConvertYUV422PToRGB24(const uint8_t* y_plane, const uint8_t* u_plane, const uint8_t* v_plane,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t y_stride, uint32_t u_stride, uint32_t v_stride,
|
||||
uint8_t* rgb_data, uint32_t rgb_stride) {
|
||||
for (uint32_t y = 0; y < height; ++y) {
|
||||
for (uint32_t x = 0; x < width; ++x) {
|
||||
// YUV 값 가져오기
|
||||
uint8_t Y = y_plane[y * y_stride + x];
|
||||
uint8_t U = u_plane[y * u_stride + (x / 2)]; // 수평만 서브샘플링
|
||||
uint8_t V = v_plane[y * v_stride + (x / 2)];
|
||||
|
||||
// YUV → RGB 변환
|
||||
int C = Y - 16;
|
||||
int D = U - 128;
|
||||
int E = V - 128;
|
||||
|
||||
int R = (298 * C + 409 * E + 128) >> 8;
|
||||
int G = (298 * C - 100 * D - 208 * E + 128) >> 8;
|
||||
int B = (298 * C + 516 * D + 128) >> 8;
|
||||
|
||||
R = std::clamp(R, 0, 255);
|
||||
G = std::clamp(G, 0, 255);
|
||||
B = std::clamp(B, 0, 255);
|
||||
|
||||
uint8_t* pixel = rgb_data + (y * rgb_stride) + (x * 3);
|
||||
pixel[0] = static_cast<uint8_t>(R);
|
||||
pixel[1] = static_cast<uint8_t>(G);
|
||||
pixel[2] = static_cast<uint8_t>(B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// YUV444P → RGB24 변환 (4:4:4 풀 해상도)
|
||||
void FileOutput::ConvertYUV444PToRGB24(const uint8_t* y_plane, const uint8_t* u_plane, const uint8_t* v_plane,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t y_stride, uint32_t u_stride, uint32_t v_stride,
|
||||
uint8_t* rgb_data, uint32_t rgb_stride) {
|
||||
for (uint32_t y = 0; y < height; ++y) {
|
||||
for (uint32_t x = 0; x < width; ++x) {
|
||||
// YUV 값 가져오기
|
||||
uint8_t Y = y_plane[y * y_stride + x];
|
||||
uint8_t U = u_plane[y * u_stride + x]; // 서브샘플링 없음
|
||||
uint8_t V = v_plane[y * v_stride + x];
|
||||
|
||||
// YUV → RGB 변환
|
||||
int C = Y - 16;
|
||||
int D = U - 128;
|
||||
int E = V - 128;
|
||||
|
||||
int R = (298 * C + 409 * E + 128) >> 8;
|
||||
int G = (298 * C - 100 * D - 208 * E + 128) >> 8;
|
||||
int B = (298 * C + 516 * D + 128) >> 8;
|
||||
|
||||
R = std::clamp(R, 0, 255);
|
||||
G = std::clamp(G, 0, 255);
|
||||
B = std::clamp(B, 0, 255);
|
||||
|
||||
uint8_t* pixel = rgb_data + (y * rgb_stride) + (x * 3);
|
||||
pixel[0] = static_cast<uint8_t>(R);
|
||||
pixel[1] = static_cast<uint8_t>(G);
|
||||
pixel[2] = static_cast<uint8_t>(B);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BMP 헤더 생성
|
||||
FileOutput::BMPHeader FileOutput::CreateBMPHeader(uint32_t width, uint32_t height) {
|
||||
BMPHeader header;
|
||||
|
||||
uint32_t row_size = width * 3;
|
||||
uint32_t padding = (4 - (row_size % 4)) % 4;
|
||||
uint32_t padded_row_size = row_size + padding;
|
||||
uint32_t image_size = padded_row_size * height;
|
||||
|
||||
header.width = static_cast<int32_t>(width);
|
||||
header.height = static_cast<int32_t>(height);
|
||||
header.size_image = image_size;
|
||||
header.file_size = 54 + image_size; // 헤더 크기 + 이미지 크기
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
// 파일 쓰기 유틸리티
|
||||
bool FileOutput::WriteDataToFile(const std::filesystem::path& file_path,
|
||||
const void* data, size_t size) {
|
||||
try {
|
||||
std::ofstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
file.write(static_cast<const char*>(data), size);
|
||||
return file.good();
|
||||
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
void FileOutput::UpdateSaveStats(bool success, size_t bytes_written, double save_time_ms) {
|
||||
std::lock_guard<std::mutex> lock(m_stats_mutex);
|
||||
|
||||
if (success) {
|
||||
m_stats.frames_saved++;
|
||||
m_stats.total_bytes_written += bytes_written;
|
||||
|
||||
// 평균 저장 시간 업데이트 (지수 이동 평균)
|
||||
const double alpha = 0.1;
|
||||
m_stats.avg_save_time_ms = alpha * save_time_ms + (1.0 - alpha) * m_stats.avg_save_time_ms;
|
||||
} else {
|
||||
m_stats.failed_saves++;
|
||||
}
|
||||
}
|
||||
|
||||
// 에러 결과 생성
|
||||
FileOutput::SaveResult FileOutput::CreateErrorResult(const std::string& error_message) {
|
||||
SaveResult result;
|
||||
result.success = false;
|
||||
result.error_message = error_message;
|
||||
std::cout << "[FileOutput Error] " << error_message << std::endl;
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
161
vav2/Vav2Player/Vav2Player/src/Output/FileOutput.h
Normal file
@@ -0,0 +1,161 @@
|
||||
#pragma once
|
||||
#include "../Common/VideoTypes.h"
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 디코딩된 프레임을 다양한 포맷으로 파일에 저장하는 클래스
|
||||
class FileOutput {
|
||||
public:
|
||||
// 출력 포맷 종류
|
||||
enum class OutputFormat {
|
||||
RawYUV, // Raw YUV420P/422P/444P 포맷
|
||||
BMP, // Windows Bitmap 포맷 (RGB24)
|
||||
PNG // PNG 포맷 (향후 확장용)
|
||||
};
|
||||
|
||||
// 출력 설정
|
||||
struct OutputConfig {
|
||||
OutputFormat format = OutputFormat::BMP;
|
||||
std::filesystem::path output_directory = "output";
|
||||
std::string filename_prefix = "frame";
|
||||
bool create_subdirectories = true;
|
||||
bool overwrite_existing = true;
|
||||
int quality = 100; // PNG/JPEG용 (향후)
|
||||
};
|
||||
|
||||
// 저장 결과 정보
|
||||
struct SaveResult {
|
||||
bool success = false;
|
||||
std::filesystem::path saved_path;
|
||||
size_t file_size_bytes = 0;
|
||||
std::string error_message;
|
||||
};
|
||||
|
||||
// 진행 상황 콜백 (프레임 번호, 저장된 파일 경로)
|
||||
using ProgressCallback = std::function<void(uint64_t frame_index, const std::filesystem::path& file_path)>;
|
||||
|
||||
public:
|
||||
explicit FileOutput(const OutputConfig& config = OutputConfig{});
|
||||
~FileOutput() = default;
|
||||
|
||||
// 복사 방지
|
||||
FileOutput(const FileOutput&) = delete;
|
||||
FileOutput& operator=(const FileOutput&) = delete;
|
||||
|
||||
// 설정 관리
|
||||
void SetConfig(const OutputConfig& config);
|
||||
const OutputConfig& GetConfig() const { return m_config; }
|
||||
|
||||
// 진행 상황 콜백 설정
|
||||
void SetProgressCallback(ProgressCallback callback);
|
||||
|
||||
// 단일 프레임 저장
|
||||
SaveResult SaveFrame(const VideoFrame& frame, uint64_t frame_index, double timestamp = 0.0);
|
||||
|
||||
// 프레임 시퀀스 저장 (StreamingPipeline과 연동용)
|
||||
bool SaveFrameSequence(const VideoFrame& frame, uint64_t frame_index, double timestamp = 0.0);
|
||||
|
||||
// 출력 디렉토리 관리
|
||||
bool CreateOutputDirectory();
|
||||
void ClearOutputDirectory();
|
||||
|
||||
// 통계 정보
|
||||
struct OutputStats {
|
||||
uint64_t frames_saved = 0;
|
||||
uint64_t total_bytes_written = 0;
|
||||
uint64_t failed_saves = 0;
|
||||
std::chrono::high_resolution_clock::time_point start_time;
|
||||
double avg_save_time_ms = 0.0;
|
||||
};
|
||||
|
||||
OutputStats GetStats() const;
|
||||
void ResetStats();
|
||||
|
||||
// 유틸리티 메서드
|
||||
static std::string FormatToString(OutputFormat format);
|
||||
static std::filesystem::path GenerateFilename(const std::string& prefix,
|
||||
uint64_t frame_index,
|
||||
OutputFormat format);
|
||||
|
||||
private:
|
||||
OutputConfig m_config;
|
||||
ProgressCallback m_progress_callback;
|
||||
|
||||
// 통계
|
||||
mutable std::mutex m_stats_mutex;
|
||||
OutputStats m_stats;
|
||||
|
||||
// 포맷별 저장 구현
|
||||
SaveResult SaveAsRawYUV(const VideoFrame& frame, const std::filesystem::path& file_path);
|
||||
SaveResult SaveAsBMP(const VideoFrame& frame, const std::filesystem::path& file_path);
|
||||
SaveResult SaveAsPNG(const VideoFrame& frame, const std::filesystem::path& file_path); // 향후
|
||||
|
||||
// YUV → RGB 변환
|
||||
struct RGBFrame {
|
||||
std::vector<uint8_t> data;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
uint32_t stride = 0; // RGB24: stride = width * 3
|
||||
};
|
||||
|
||||
bool ConvertYUVToRGB(const VideoFrame& yuv_frame, RGBFrame& rgb_frame);
|
||||
|
||||
// YUV420P → RGB24 변환
|
||||
void ConvertYUV420PToRGB24(const uint8_t* y_plane, const uint8_t* u_plane, const uint8_t* v_plane,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t y_stride, uint32_t u_stride, uint32_t v_stride,
|
||||
uint8_t* rgb_data, uint32_t rgb_stride);
|
||||
|
||||
// YUV422P → RGB24 변환
|
||||
void ConvertYUV422PToRGB24(const uint8_t* y_plane, const uint8_t* u_plane, const uint8_t* v_plane,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t y_stride, uint32_t u_stride, uint32_t v_stride,
|
||||
uint8_t* rgb_data, uint32_t rgb_stride);
|
||||
|
||||
// YUV444P → RGB24 변환
|
||||
void ConvertYUV444PToRGB24(const uint8_t* y_plane, const uint8_t* u_plane, const uint8_t* v_plane,
|
||||
uint32_t width, uint32_t height,
|
||||
uint32_t y_stride, uint32_t u_stride, uint32_t v_stride,
|
||||
uint8_t* rgb_data, uint32_t rgb_stride);
|
||||
|
||||
// BMP 파일 헤더 생성
|
||||
struct BMPHeader {
|
||||
// BITMAPFILEHEADER (14 bytes)
|
||||
uint16_t file_type = 0x4D42; // "BM"
|
||||
uint32_t file_size = 0;
|
||||
uint16_t reserved1 = 0;
|
||||
uint16_t reserved2 = 0;
|
||||
uint32_t offset_data = 54; // 14 + 40
|
||||
|
||||
// BITMAPINFOHEADER (40 bytes)
|
||||
uint32_t size = 40;
|
||||
int32_t width = 0;
|
||||
int32_t height = 0; // 음수면 top-down
|
||||
uint16_t planes = 1;
|
||||
uint16_t bit_count = 24; // RGB24
|
||||
uint32_t compression = 0; // BI_RGB
|
||||
uint32_t size_image = 0;
|
||||
int32_t x_pixels_per_meter = 2835; // 72 DPI
|
||||
int32_t y_pixels_per_meter = 2835;
|
||||
uint32_t colors_used = 0;
|
||||
uint32_t colors_important = 0;
|
||||
};
|
||||
|
||||
BMPHeader CreateBMPHeader(uint32_t width, uint32_t height);
|
||||
|
||||
// 파일 I/O 유틸리티
|
||||
bool WriteDataToFile(const std::filesystem::path& file_path,
|
||||
const void* data, size_t size);
|
||||
|
||||
// 통계 업데이트
|
||||
void UpdateSaveStats(bool success, size_t bytes_written, double save_time_ms);
|
||||
|
||||
// 에러 처리
|
||||
SaveResult CreateErrorResult(const std::string& error_message);
|
||||
};
|
||||
|
||||
} // namespace Vav2Player
|
||||
235
vav2/Vav2Player/Vav2Player/src/Pipeline/FrameBuffer.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include "pch.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
FrameBuffer::FrameBuffer(size_t max_capacity)
|
||||
: m_max_capacity(max_capacity) {
|
||||
m_stats.max_size = max_capacity;
|
||||
}
|
||||
|
||||
FrameBuffer::~FrameBuffer() {
|
||||
Stop();
|
||||
Clear();
|
||||
}
|
||||
|
||||
bool FrameBuffer::PushFrame(FrameEntry&& frame_entry, std::chrono::milliseconds timeout) {
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_stopped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 타임아웃 대기로 버퍼가 비기를 기다림
|
||||
if (!m_not_full_cv.wait_for(lock, timeout, [this]() {
|
||||
return m_stopped || m_buffer.size() < m_max_capacity;
|
||||
})) {
|
||||
// 타임아웃 발생
|
||||
if (m_drop_old_frames && !m_buffer.empty()) {
|
||||
// 가장 오래된 프레임 드롭하고 새 프레임 추가
|
||||
m_buffer.pop();
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
m_stats.frames_dropped++;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_stopped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 프레임 추가
|
||||
m_buffer.push(std::move(frame_entry));
|
||||
UpdatePushStats();
|
||||
|
||||
// Consumer 스레드에게 알림
|
||||
m_not_empty_cv.notify_one();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FrameBuffer::PopFrame(FrameEntry& frame_entry, std::chrono::milliseconds timeout) {
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_stopped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 타임아웃 대기로 버퍼에 프레임이 있기를 기다림
|
||||
if (!m_not_empty_cv.wait_for(lock, timeout, [this]() {
|
||||
return m_stopped || !m_buffer.empty();
|
||||
})) {
|
||||
// 타임아웃 발생
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_stopped || m_buffer.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 프레임 가져오기
|
||||
frame_entry = std::move(m_buffer.front());
|
||||
m_buffer.pop();
|
||||
UpdatePopStats();
|
||||
|
||||
// Producer 스레드에게 알림
|
||||
m_not_full_cv.notify_one();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FrameBuffer::TryPushFrame(FrameEntry&& frame_entry) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_stopped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_buffer.size() >= m_max_capacity) {
|
||||
if (m_drop_old_frames && !m_buffer.empty()) {
|
||||
m_buffer.pop();
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
m_stats.frames_dropped++;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
m_buffer.push(std::move(frame_entry));
|
||||
UpdatePushStats();
|
||||
m_not_empty_cv.notify_one();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FrameBuffer::TryPopFrame(FrameEntry& frame_entry) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (m_stopped || m_buffer.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frame_entry = std::move(m_buffer.front());
|
||||
m_buffer.pop();
|
||||
UpdatePopStats();
|
||||
m_not_full_cv.notify_one();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FrameBuffer::Clear() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
// 모든 프레임 제거
|
||||
while (!m_buffer.empty()) {
|
||||
m_buffer.pop();
|
||||
}
|
||||
|
||||
// 대기 중인 스레드들에게 알림
|
||||
m_not_full_cv.notify_all();
|
||||
m_not_empty_cv.notify_all();
|
||||
}
|
||||
|
||||
void FrameBuffer::Stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_stopped = true;
|
||||
}
|
||||
|
||||
// 모든 대기 중인 스레드 깨우기
|
||||
m_not_full_cv.notify_all();
|
||||
m_not_empty_cv.notify_all();
|
||||
}
|
||||
|
||||
void FrameBuffer::Resume() {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_stopped = false;
|
||||
}
|
||||
|
||||
bool FrameBuffer::IsEmpty() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_buffer.empty();
|
||||
}
|
||||
|
||||
bool FrameBuffer::IsFull() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_buffer.size() >= m_max_capacity;
|
||||
}
|
||||
|
||||
size_t FrameBuffer::Size() const {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
return m_buffer.size();
|
||||
}
|
||||
|
||||
size_t FrameBuffer::Capacity() const {
|
||||
return m_max_capacity;
|
||||
}
|
||||
|
||||
bool FrameBuffer::IsStopped() const {
|
||||
return m_stopped;
|
||||
}
|
||||
|
||||
FrameBuffer::BufferStats FrameBuffer::GetStats() const {
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
BufferStats stats = m_stats;
|
||||
|
||||
// 현재 버퍼 크기 업데이트
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
stats.current_size = m_buffer.size();
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
void FrameBuffer::ResetStats() {
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
m_stats = BufferStats();
|
||||
m_stats.max_size = m_max_capacity;
|
||||
}
|
||||
|
||||
void FrameBuffer::SetMaxCapacity(size_t new_capacity) {
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_max_capacity = new_capacity;
|
||||
|
||||
// 현재 버퍼가 새 용량보다 크면 오래된 프레임들 제거
|
||||
while (m_buffer.size() > m_max_capacity && !m_buffer.empty()) {
|
||||
m_buffer.pop();
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
m_stats.frames_dropped++;
|
||||
}
|
||||
|
||||
// 통계 업데이트
|
||||
{
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
m_stats.max_size = new_capacity;
|
||||
}
|
||||
|
||||
// Producer 스레드들에게 공간이 생겼음을 알림
|
||||
m_not_full_cv.notify_all();
|
||||
}
|
||||
|
||||
void FrameBuffer::UpdatePushStats() {
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
m_stats.total_frames_pushed++;
|
||||
m_last_push_time = std::chrono::high_resolution_clock::now();
|
||||
UpdateBufferFillRatio();
|
||||
}
|
||||
|
||||
void FrameBuffer::UpdatePopStats() {
|
||||
std::lock_guard<std::mutex> stats_lock(m_stats_mutex);
|
||||
m_stats.total_frames_popped++;
|
||||
m_last_pop_time = std::chrono::high_resolution_clock::now();
|
||||
UpdateBufferFillRatio();
|
||||
}
|
||||
|
||||
void FrameBuffer::UpdateBufferFillRatio() {
|
||||
// 현재 버퍼 사용률 계산 (이동 평균)
|
||||
double current_ratio = static_cast<double>(m_buffer.size()) / m_max_capacity;
|
||||
const double alpha = 0.1; // 지수 이동 평균 가중치
|
||||
m_stats.avg_buffer_fill_ratio = alpha * current_ratio + (1.0 - alpha) * m_stats.avg_buffer_fill_ratio;
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
121
vav2/Vav2Player/Vav2Player/src/Pipeline/FrameBuffer.h
Normal file
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
#include "../Common/VideoTypes.h"
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 멀티스레드 환경에서 프레임을 안전하게 관리하는 버퍼 클래스
|
||||
// Producer-Consumer 패턴을 지원하며 백프레셔 핸들링 포함
|
||||
class FrameBuffer {
|
||||
public:
|
||||
// 프레임 버퍼 항목 - 프레임과 메타데이터를 함께 저장
|
||||
struct FrameEntry {
|
||||
VideoFrame frame;
|
||||
uint64_t frame_index = 0;
|
||||
double timestamp_seconds = 0.0;
|
||||
std::chrono::high_resolution_clock::time_point decode_time;
|
||||
bool is_keyframe = false;
|
||||
|
||||
FrameEntry() = default;
|
||||
FrameEntry(FrameEntry&& other) noexcept
|
||||
: frame(std::move(other.frame))
|
||||
, frame_index(other.frame_index)
|
||||
, timestamp_seconds(other.timestamp_seconds)
|
||||
, decode_time(other.decode_time)
|
||||
, is_keyframe(other.is_keyframe) {
|
||||
}
|
||||
|
||||
FrameEntry& operator=(FrameEntry&& other) noexcept {
|
||||
if (this != &other) {
|
||||
frame = std::move(other.frame);
|
||||
frame_index = other.frame_index;
|
||||
timestamp_seconds = other.timestamp_seconds;
|
||||
decode_time = other.decode_time;
|
||||
is_keyframe = other.is_keyframe;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// 복사 방지
|
||||
FrameEntry(const FrameEntry&) = delete;
|
||||
FrameEntry& operator=(const FrameEntry&) = delete;
|
||||
};
|
||||
|
||||
// 버퍼 통계 정보
|
||||
struct BufferStats {
|
||||
size_t current_size = 0;
|
||||
size_t max_size = 0;
|
||||
uint64_t total_frames_pushed = 0;
|
||||
uint64_t total_frames_popped = 0;
|
||||
uint64_t frames_dropped = 0;
|
||||
double avg_buffer_fill_ratio = 0.0;
|
||||
std::chrono::milliseconds avg_wait_time{0};
|
||||
};
|
||||
|
||||
public:
|
||||
explicit FrameBuffer(size_t max_capacity = 15); // 기본 15프레임 = 0.5초@30fps
|
||||
~FrameBuffer();
|
||||
|
||||
// 복사 방지
|
||||
FrameBuffer(const FrameBuffer&) = delete;
|
||||
FrameBuffer& operator=(const FrameBuffer&) = delete;
|
||||
|
||||
// 프레임 추가 (Producer 스레드에서 호출)
|
||||
bool PushFrame(FrameEntry&& frame_entry,
|
||||
std::chrono::milliseconds timeout = std::chrono::milliseconds(100));
|
||||
|
||||
// 프레임 가져오기 (Consumer 스레드에서 호출)
|
||||
bool PopFrame(FrameEntry& frame_entry,
|
||||
std::chrono::milliseconds timeout = std::chrono::milliseconds(100));
|
||||
|
||||
// 논블로킹 버전
|
||||
bool TryPushFrame(FrameEntry&& frame_entry);
|
||||
bool TryPopFrame(FrameEntry& frame_entry);
|
||||
|
||||
// 버퍼 상태 관리
|
||||
void Clear();
|
||||
void Stop(); // 모든 대기 중인 스레드를 깨움
|
||||
void Resume();
|
||||
|
||||
// 상태 조회
|
||||
bool IsEmpty() const;
|
||||
bool IsFull() const;
|
||||
size_t Size() const;
|
||||
size_t Capacity() const;
|
||||
bool IsStopped() const;
|
||||
|
||||
// 통계 정보
|
||||
BufferStats GetStats() const;
|
||||
void ResetStats();
|
||||
|
||||
// 설정 변경
|
||||
void SetMaxCapacity(size_t new_capacity);
|
||||
void SetDropOldFramesOnFull(bool enable) { m_drop_old_frames = enable; }
|
||||
|
||||
private:
|
||||
mutable std::mutex m_mutex;
|
||||
std::condition_variable m_not_full_cv;
|
||||
std::condition_variable m_not_empty_cv;
|
||||
|
||||
std::queue<FrameEntry> m_buffer;
|
||||
size_t m_max_capacity;
|
||||
std::atomic<bool> m_stopped{false};
|
||||
bool m_drop_old_frames = true; // 버퍼가 가득 찰 때 오래된 프레임 드롭
|
||||
|
||||
// 통계 정보
|
||||
mutable std::mutex m_stats_mutex;
|
||||
BufferStats m_stats;
|
||||
std::chrono::high_resolution_clock::time_point m_last_push_time;
|
||||
std::chrono::high_resolution_clock::time_point m_last_pop_time;
|
||||
|
||||
// 내부 helper 메서드
|
||||
void UpdatePushStats();
|
||||
void UpdatePopStats();
|
||||
void UpdateBufferFillRatio();
|
||||
};
|
||||
|
||||
} // namespace Vav2Player
|
||||
265
vav2/Vav2Player/Vav2Player/src/Pipeline/StreamingPipeline.cpp
Normal file
@@ -0,0 +1,265 @@
|
||||
#include "pch.h"
|
||||
#include "StreamingPipeline.h"
|
||||
#include "../Decoder/VideoDecoderFactory.h"
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
StreamingPipeline::StreamingPipeline(const PipelineConfig& config)
|
||||
: m_config(config) {
|
||||
m_stats.target_fps = config.target_fps;
|
||||
}
|
||||
|
||||
StreamingPipeline::~StreamingPipeline() {
|
||||
Stop();
|
||||
CleanupComponents();
|
||||
}
|
||||
|
||||
bool StreamingPipeline::Initialize(WebMFileReader* file_reader,
|
||||
std::unique_ptr<IVideoDecoder> decoder) {
|
||||
if (!file_reader || !decoder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetState(State::Initializing);
|
||||
|
||||
m_file_reader = file_reader;
|
||||
m_decoder = std::move(decoder);
|
||||
|
||||
if (!InitializeComponents()) {
|
||||
SetState(State::Error);
|
||||
return false;
|
||||
}
|
||||
|
||||
SetState(State::Stopped);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StreamingPipeline::Start() {
|
||||
if (m_state != State::Stopped) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_file_reader || !m_decoder || !m_frame_buffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetState(State::Running);
|
||||
m_should_stop = false;
|
||||
|
||||
// 타이밍 상태 초기화
|
||||
ResetTimingState();
|
||||
|
||||
// Producer 스레드 시작 (파일 읽기 + 디코딩)
|
||||
m_producer_thread = std::make_unique<std::thread>(&StreamingPipeline::ProducerThreadFunc, this);
|
||||
|
||||
// Consumer 스레드 시작 (타이밍 제어 + 출력)
|
||||
m_consumer_thread = std::make_unique<std::thread>(&StreamingPipeline::ConsumerThreadFunc, this);
|
||||
|
||||
std::cout << "[StreamingPipeline] Started - Target FPS: " << m_config.target_fps
|
||||
<< ", Buffer size: " << m_config.buffer_size << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool StreamingPipeline::Stop() {
|
||||
if (m_state == State::Stopped) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cout << "[StreamingPipeline] Stopping..." << std::endl;
|
||||
|
||||
m_should_stop = true;
|
||||
SetState(State::Stopped);
|
||||
|
||||
// 프레임 버퍼 중지 (대기 중인 스레드들 깨우기)
|
||||
if (m_frame_buffer) {
|
||||
m_frame_buffer->Stop();
|
||||
}
|
||||
|
||||
// 스레드 종료 대기
|
||||
if (m_producer_thread && m_producer_thread->joinable()) {
|
||||
m_producer_thread->join();
|
||||
m_producer_thread.reset();
|
||||
}
|
||||
|
||||
if (m_consumer_thread && m_consumer_thread->joinable()) {
|
||||
m_consumer_thread->join();
|
||||
m_consumer_thread.reset();
|
||||
}
|
||||
|
||||
// 프레임 버퍼 정리
|
||||
if (m_frame_buffer) {
|
||||
m_frame_buffer->Clear();
|
||||
m_frame_buffer->Resume();
|
||||
}
|
||||
|
||||
std::cout << "[StreamingPipeline] Stopped" << std::endl;
|
||||
return true;
|
||||
}
|
||||
|
||||
void StreamingPipeline::SetFrameCallback(FrameCallback callback) {
|
||||
m_frame_callback = std::move(callback);
|
||||
}
|
||||
|
||||
StreamingPipeline::State StreamingPipeline::GetState() const {
|
||||
return m_state;
|
||||
}
|
||||
|
||||
bool StreamingPipeline::IsRunning() const {
|
||||
return m_state == State::Running;
|
||||
}
|
||||
|
||||
StreamingPipeline::PlaybackStats StreamingPipeline::GetStats() const {
|
||||
std::lock_guard<std::mutex> lock(m_stats_mutex);
|
||||
PlaybackStats stats = m_stats;
|
||||
|
||||
// 실시간 데이터 업데이트
|
||||
stats.current_state = m_state;
|
||||
stats.current_timestamp = m_current_timestamp;
|
||||
|
||||
if (m_frame_buffer) {
|
||||
stats.buffer_stats = m_frame_buffer->GetStats();
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
bool StreamingPipeline::InitializeComponents() {
|
||||
// 프레임 버퍼 생성
|
||||
m_frame_buffer = std::make_unique<FrameBuffer>(m_config.buffer_size);
|
||||
m_frame_buffer->SetDropOldFramesOnFull(m_config.drop_frames_on_delay);
|
||||
|
||||
// 통계 초기화
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stats_mutex);
|
||||
m_stats = PlaybackStats{};
|
||||
m_stats.target_fps = m_config.target_fps;
|
||||
m_stats.start_time = std::chrono::high_resolution_clock::now();
|
||||
|
||||
if (m_file_reader) {
|
||||
auto metadata = m_file_reader->GetVideoMetadata();
|
||||
m_stats.total_duration = metadata.duration_seconds;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void StreamingPipeline::CleanupComponents() {
|
||||
m_frame_buffer.reset();
|
||||
m_decoder.reset();
|
||||
m_file_reader = nullptr;
|
||||
}
|
||||
|
||||
void StreamingPipeline::ResetTimingState() {
|
||||
m_playback_start_time = std::chrono::high_resolution_clock::now();
|
||||
m_last_frame_time = m_playback_start_time;
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_stats_mutex);
|
||||
m_stats.start_time = m_playback_start_time;
|
||||
}
|
||||
|
||||
void StreamingPipeline::SetState(State new_state) {
|
||||
m_state = new_state;
|
||||
}
|
||||
|
||||
void StreamingPipeline::ProducerThreadFunc() {
|
||||
std::cout << "[Producer] Thread started" << std::endl;
|
||||
|
||||
while (!m_should_stop && m_state != State::Stopped) {
|
||||
if (m_state == State::Paused || m_state == State::Seeking) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 패킷 읽기
|
||||
VideoPacket packet;
|
||||
if (!m_file_reader->ReadNextPacket(packet)) {
|
||||
if (m_file_reader->IsEndOfFile()) {
|
||||
std::cout << "[Producer] End of file reached" << std::endl;
|
||||
break;
|
||||
} else {
|
||||
std::cout << "[Producer] Failed to read packet" << std::endl;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 디코딩
|
||||
VideoFrame decoded_frame;
|
||||
if (!m_decoder->DecodeFrame(packet, decoded_frame)) {
|
||||
// EAGAIN은 정상적인 상황 (더 많은 데이터 필요)
|
||||
continue;
|
||||
}
|
||||
|
||||
// FrameEntry 생성
|
||||
FrameBuffer::FrameEntry frame_entry;
|
||||
frame_entry.frame = std::move(decoded_frame);
|
||||
frame_entry.frame_index = packet.frame_index;
|
||||
frame_entry.timestamp_seconds = packet.timestamp_seconds;
|
||||
frame_entry.decode_time = std::chrono::high_resolution_clock::now();
|
||||
frame_entry.is_keyframe = packet.is_keyframe;
|
||||
|
||||
// 프레임 버퍼에 추가
|
||||
if (!m_frame_buffer->PushFrame(std::move(frame_entry), m_config.decode_timeout)) {
|
||||
std::cout << "[Producer] Failed to push frame to buffer (timeout)" << std::endl;
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "[Producer Error] " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[Producer] Thread finished" << std::endl;
|
||||
}
|
||||
|
||||
void StreamingPipeline::ConsumerThreadFunc() {
|
||||
std::cout << "[Consumer] Thread started" << std::endl;
|
||||
|
||||
while (!m_should_stop && m_state != State::Stopped) {
|
||||
if (m_state == State::Paused || m_state == State::Seeking) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 프레임 버퍼에서 프레임 가져오기
|
||||
FrameBuffer::FrameEntry frame_entry;
|
||||
if (!m_frame_buffer->PopFrame(frame_entry, m_config.frame_timeout)) {
|
||||
// 타임아웃 또는 버퍼 비어있음
|
||||
continue;
|
||||
}
|
||||
|
||||
// 간단한 타이밍 제어 (33.33ms = 30fps)
|
||||
auto frame_interval = std::chrono::milliseconds(static_cast<long long>(1000.0 / m_config.target_fps));
|
||||
std::this_thread::sleep_for(frame_interval);
|
||||
|
||||
// 프레임 출력 (콜백 호출)
|
||||
if (m_frame_callback) {
|
||||
m_frame_callback(frame_entry.frame,
|
||||
frame_entry.frame_index,
|
||||
frame_entry.timestamp_seconds);
|
||||
}
|
||||
|
||||
// 상태 업데이트
|
||||
m_current_timestamp = frame_entry.timestamp_seconds;
|
||||
m_current_frame_index = frame_entry.frame_index;
|
||||
|
||||
// 통계 업데이트
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_stats_mutex);
|
||||
m_stats.frames_displayed++;
|
||||
m_stats.current_timestamp = frame_entry.timestamp_seconds;
|
||||
}
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
std::cout << "[Consumer Error] " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[Consumer] Thread finished" << std::endl;
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
151
vav2/Vav2Player/Vav2Player/src/Pipeline/StreamingPipeline.h
Normal file
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
#include "../Common/VideoTypes.h"
|
||||
#include "../FileIO/WebMFileReader.h"
|
||||
#include "../Decoder/IVideoDecoder.h"
|
||||
#include "FrameBuffer.h"
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 30fps 실시간 재생을 위한 멀티스레드 스트리밍 파이프라인
|
||||
// Producer: 파일 읽기 + 디코딩, Consumer: 타이밍 제어 + 출력
|
||||
class StreamingPipeline {
|
||||
public:
|
||||
// 파이프라인 설정
|
||||
struct PipelineConfig {
|
||||
double target_fps = 30.0; // 목표 FPS
|
||||
size_t buffer_size = 15; // 버퍼 크기 (프레임 수)
|
||||
bool drop_frames_on_delay = true; // 지연 시 프레임 드롭 허용
|
||||
std::chrono::milliseconds frame_timeout{1000}; // 프레임 대기 타임아웃
|
||||
std::chrono::milliseconds decode_timeout{500}; // 디코딩 타임아웃
|
||||
bool enable_performance_monitoring = true; // 성능 모니터링 활성화
|
||||
};
|
||||
|
||||
// 파이프라인 상태
|
||||
enum class State {
|
||||
Stopped,
|
||||
Initializing,
|
||||
Running,
|
||||
Paused,
|
||||
Seeking,
|
||||
Error
|
||||
};
|
||||
|
||||
// 재생 통계
|
||||
struct PlaybackStats {
|
||||
State current_state = State::Stopped;
|
||||
double current_fps = 0.0;
|
||||
double target_fps = 30.0;
|
||||
uint64_t frames_displayed = 0;
|
||||
uint64_t frames_dropped = 0;
|
||||
uint64_t frames_decoded = 0;
|
||||
double current_timestamp = 0.0;
|
||||
double total_duration = 0.0;
|
||||
double avg_decode_time_ms = 0.0;
|
||||
double avg_display_time_ms = 0.0;
|
||||
FrameBuffer::BufferStats buffer_stats;
|
||||
std::chrono::high_resolution_clock::time_point start_time;
|
||||
};
|
||||
|
||||
// 프레임 출력 콜백 함수 타입
|
||||
// 실제 렌더링이나 파일 저장을 위해 사용
|
||||
using FrameCallback = std::function<void(const VideoFrame& frame,
|
||||
uint64_t frame_index,
|
||||
double timestamp)>;
|
||||
|
||||
public:
|
||||
explicit StreamingPipeline(const PipelineConfig& config = PipelineConfig{});
|
||||
~StreamingPipeline();
|
||||
|
||||
// 복사 방지
|
||||
StreamingPipeline(const StreamingPipeline&) = delete;
|
||||
StreamingPipeline& operator=(const StreamingPipeline&) = delete;
|
||||
|
||||
// 파이프라인 초기화
|
||||
bool Initialize(WebMFileReader* file_reader,
|
||||
std::unique_ptr<IVideoDecoder> decoder);
|
||||
|
||||
// 재생 제어
|
||||
bool Start();
|
||||
bool Pause();
|
||||
bool Resume();
|
||||
bool Stop();
|
||||
bool Seek(double timestamp_seconds);
|
||||
bool SeekToFrame(uint64_t frame_index);
|
||||
|
||||
// 프레임 출력 콜백 설정
|
||||
void SetFrameCallback(FrameCallback callback);
|
||||
|
||||
// 상태 조회
|
||||
State GetState() const;
|
||||
bool IsRunning() const;
|
||||
PlaybackStats GetStats() const;
|
||||
|
||||
// 설정 변경
|
||||
void SetTargetFPS(double fps);
|
||||
void SetBufferSize(size_t size);
|
||||
void SetDropFramesOnDelay(bool enable);
|
||||
|
||||
// 고급 제어
|
||||
bool SetPlaybackSpeed(double speed); // 1.0 = 정상속도, 2.0 = 2배속
|
||||
void EnableFrameDropping(bool enable);
|
||||
void EnablePerformanceMonitoring(bool enable);
|
||||
|
||||
private:
|
||||
// 설정 및 상태
|
||||
PipelineConfig m_config;
|
||||
std::atomic<State> m_state{State::Stopped};
|
||||
std::atomic<bool> m_should_stop{false};
|
||||
std::atomic<double> m_playback_speed{1.0};
|
||||
|
||||
// 컴포넌트들
|
||||
WebMFileReader* m_file_reader = nullptr;
|
||||
std::unique_ptr<IVideoDecoder> m_decoder;
|
||||
std::unique_ptr<FrameBuffer> m_frame_buffer;
|
||||
FrameCallback m_frame_callback;
|
||||
|
||||
// 스레드들
|
||||
std::unique_ptr<std::thread> m_producer_thread;
|
||||
std::unique_ptr<std::thread> m_consumer_thread;
|
||||
|
||||
// 타이밍 제어
|
||||
std::chrono::high_resolution_clock::time_point m_playback_start_time;
|
||||
std::chrono::high_resolution_clock::time_point m_last_frame_time;
|
||||
std::atomic<double> m_current_timestamp{0.0};
|
||||
std::atomic<uint64_t> m_current_frame_index{0};
|
||||
|
||||
// 통계
|
||||
mutable std::mutex m_stats_mutex;
|
||||
PlaybackStats m_stats;
|
||||
|
||||
// 스레드 함수들
|
||||
void ProducerThreadFunc();
|
||||
void ConsumerThreadFunc();
|
||||
|
||||
// 내부 helper 메서드들
|
||||
bool InitializeComponents();
|
||||
void CleanupComponents();
|
||||
void ResetTimingState();
|
||||
void UpdatePlaybackStats();
|
||||
|
||||
// 타이밍 계산
|
||||
std::chrono::milliseconds CalculateFrameInterval() const;
|
||||
std::chrono::milliseconds CalculateNextFrameDelay(double frame_timestamp) const;
|
||||
bool ShouldDropFrame(double frame_timestamp) const;
|
||||
|
||||
// 에러 처리
|
||||
void HandleProducerError(const std::string& error);
|
||||
void HandleConsumerError(const std::string& error);
|
||||
void SetState(State new_state);
|
||||
|
||||
// 성능 모니터링
|
||||
void UpdateFPSCalculation();
|
||||
void UpdateDecodeTimeStats(double decode_time_ms);
|
||||
void UpdateDisplayTimeStats(double display_time_ms);
|
||||
};
|
||||
|
||||
} // namespace Vav2Player
|
||||
202
vav2/Vav2Player/Vav2Player/src/TestMain.cpp
Normal file
@@ -0,0 +1,202 @@
|
||||
#include "pch.h"
|
||||
#include "FileIO/WebMFileReader.h"
|
||||
#include "Decoder/VideoDecoderFactory.h"
|
||||
#include "Decoder/AV1Decoder.h"
|
||||
#include "Output/FileOutput.h"
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
using namespace Vav2Player;
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 간단한 통합 테스트 함수
|
||||
bool TestWebMFileReaderAndAV1Decoder(const std::string& webm_file_path) {
|
||||
std::cout << "[Test] Starting WebMFileReader + AV1Decoder integration test..." << std::endl;
|
||||
|
||||
// 1. WebMFileReader 테스트
|
||||
WebMFileReader reader;
|
||||
|
||||
std::cout << "[Test] Opening WebM file: " << webm_file_path << std::endl;
|
||||
if (!reader.OpenFile(webm_file_path)) {
|
||||
std::cout << "[Test] Failed to open WebM file: " << reader.GetLastErrorString() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 비디오 트랙 정보 출력
|
||||
auto tracks = reader.GetVideoTracks();
|
||||
std::cout << "[Test] Found " << tracks.size() << " video tracks:" << std::endl;
|
||||
for (const auto& track : tracks) {
|
||||
std::cout << " Track " << track.track_number << ": "
|
||||
<< track.codec_name << " (" << track.codec_id << ") "
|
||||
<< track.width << "x" << track.height << " @" << track.frame_rate << "fps" << std::endl;
|
||||
}
|
||||
|
||||
if (tracks.empty()) {
|
||||
std::cout << "[Test] No video tracks found!" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 메타데이터 출력
|
||||
const auto& metadata = reader.GetVideoMetadata();
|
||||
std::cout << "[Test] Video metadata:" << std::endl;
|
||||
std::cout << " Resolution: " << metadata.width << "x" << metadata.height << std::endl;
|
||||
std::cout << " Duration: " << metadata.duration_seconds << " seconds" << std::endl;
|
||||
std::cout << " Total frames: " << metadata.total_frames << std::endl;
|
||||
std::cout << " Codec: " << metadata.codec_name << std::endl;
|
||||
|
||||
// 2. AV1Decoder 테스트
|
||||
std::cout << "[Test] Creating AV1 decoder..." << std::endl;
|
||||
auto decoder = VideoDecoderFactory::CreateDecoder(metadata.codec_type);
|
||||
if (!decoder) {
|
||||
std::cout << "[Test] Failed to create decoder for codec type: "
|
||||
<< static_cast<int>(metadata.codec_type) << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "[Test] Initializing decoder..." << std::endl;
|
||||
if (!decoder->Initialize(metadata)) {
|
||||
std::cout << "[Test] Failed to initialize decoder" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "[Test] Decoder info: " << decoder->GetCodecName()
|
||||
<< " v" << decoder->GetVersion() << std::endl;
|
||||
|
||||
// 3. 패킷 읽기 및 디코딩 테스트 (최대 5프레임)
|
||||
std::cout << "[Test] Testing packet reading and decoding..." << std::endl;
|
||||
int frames_decoded = 0;
|
||||
const int max_test_frames = 5;
|
||||
VideoFrame last_decoded_frame; // FileOutput 테스트용
|
||||
|
||||
for (int i = 0; i < max_test_frames; ++i) {
|
||||
VideoPacket packet;
|
||||
if (!reader.ReadNextPacket(packet)) {
|
||||
if (reader.IsEndOfFile()) {
|
||||
std::cout << "[Test] Reached end of file after " << i << " packets" << std::endl;
|
||||
break;
|
||||
} else {
|
||||
std::cout << "[Test] Failed to read packet " << i << ": "
|
||||
<< reader.GetLastErrorString() << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "[Test] Read packet " << i << ": " << packet.size << " bytes, "
|
||||
<< "frame " << packet.frame_index << ", "
|
||||
<< "time " << packet.timestamp_seconds << "s, "
|
||||
<< (packet.is_keyframe ? "KEY" : "NON-KEY") << std::endl;
|
||||
|
||||
// 디코딩 시도
|
||||
VideoFrame frame;
|
||||
if (decoder->DecodeFrame(packet, frame)) {
|
||||
frames_decoded++;
|
||||
std::cout << "[Test] Successfully decoded frame " << frames_decoded
|
||||
<< ": " << frame.width << "x" << frame.height
|
||||
<< " (YUV size: " << frame.y_size << "+" << frame.u_size << "+" << frame.v_size << ")" << std::endl;
|
||||
|
||||
// 마지막 디코딩된 프레임 저장 (이동 시맨틱)
|
||||
last_decoded_frame = std::move(frame);
|
||||
} else {
|
||||
std::cout << "[Test] Failed to decode packet " << i << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// 통계 출력
|
||||
auto stats = decoder->GetStats();
|
||||
std::cout << "[Test] Decoder statistics:" << std::endl;
|
||||
std::cout << " Frames decoded: " << stats.frames_decoded << std::endl;
|
||||
std::cout << " Decode errors: " << stats.decode_errors << std::endl;
|
||||
std::cout << " Average decode time: " << stats.avg_decode_time_ms << " ms" << std::endl;
|
||||
|
||||
// 4. FileOutput 테스트
|
||||
std::cout << "\n[Test] === FileOutput Test ===" << std::endl;
|
||||
if (frames_decoded > 0 && last_decoded_frame.is_valid) {
|
||||
std::cout << "[Test] Testing FileOutput with last decoded frame..." << std::endl;
|
||||
|
||||
// FileOutput 설정
|
||||
FileOutput::OutputConfig config;
|
||||
config.output_directory = "test_output";
|
||||
config.filename_prefix = "test_frame";
|
||||
config.format = FileOutput::OutputFormat::BMP;
|
||||
config.overwrite_existing = true;
|
||||
|
||||
FileOutput file_output(config);
|
||||
|
||||
// 진행 상황 콜백 설정
|
||||
file_output.SetProgressCallback([](uint64_t frame_index, const std::filesystem::path& file_path) {
|
||||
std::cout << "[FileOutput] Saved frame " << frame_index << " to: " << file_path.string() << std::endl;
|
||||
});
|
||||
|
||||
// BMP 파일로 저장 테스트
|
||||
auto bmp_result = file_output.SaveFrame(last_decoded_frame, frames_decoded - 1, 0.0);
|
||||
std::cout << "[Test] BMP save result: " << (bmp_result.success ? "SUCCESS" : "FAILED") << std::endl;
|
||||
if (bmp_result.success) {
|
||||
std::cout << "[Test] BMP file saved to: " << bmp_result.saved_path.string() << std::endl;
|
||||
std::cout << "[Test] File size: " << bmp_result.file_size_bytes << " bytes" << std::endl;
|
||||
} else {
|
||||
std::cout << "[Test] BMP save error: " << bmp_result.error_message << std::endl;
|
||||
}
|
||||
|
||||
// Raw YUV 파일로 저장 테스트
|
||||
config.format = FileOutput::OutputFormat::RawYUV;
|
||||
file_output.SetConfig(config);
|
||||
|
||||
auto yuv_result = file_output.SaveFrame(last_decoded_frame, frames_decoded - 1, 0.0);
|
||||
std::cout << "[Test] Raw YUV save result: " << (yuv_result.success ? "SUCCESS" : "FAILED") << std::endl;
|
||||
if (yuv_result.success) {
|
||||
std::cout << "[Test] YUV file saved to: " << yuv_result.saved_path.string() << std::endl;
|
||||
std::cout << "[Test] File size: " << yuv_result.file_size_bytes << " bytes" << std::endl;
|
||||
} else {
|
||||
std::cout << "[Test] YUV save error: " << yuv_result.error_message << std::endl;
|
||||
}
|
||||
|
||||
// 통계 출력
|
||||
auto file_stats = file_output.GetStats();
|
||||
std::cout << "[Test] FileOutput Stats:" << std::endl;
|
||||
std::cout << " Frames saved: " << file_stats.frames_saved << std::endl;
|
||||
std::cout << " Total bytes written: " << file_stats.total_bytes_written << std::endl;
|
||||
std::cout << " Failed saves: " << file_stats.failed_saves << std::endl;
|
||||
std::cout << " Average save time: " << file_stats.avg_save_time_ms << " ms" << std::endl;
|
||||
} else {
|
||||
std::cout << "[Test] No valid frame available for FileOutput test" << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "[Test] Integration test completed successfully!" << std::endl;
|
||||
return frames_decoded > 0;
|
||||
}
|
||||
|
||||
// 테스트 파일이 있는지 확인하고 테스트 실행
|
||||
bool RunIntegrationTests() {
|
||||
std::cout << "[Test] === AV1 Video Player Integration Tests ===" << std::endl;
|
||||
|
||||
// 현재 디렉토리에서 테스트 파일 찾기
|
||||
std::vector<std::string> test_files = {
|
||||
"test.webm",
|
||||
"sample.webm",
|
||||
"test_av1.webm",
|
||||
"D:\\test.webm",
|
||||
"C:\\test.webm"
|
||||
};
|
||||
|
||||
std::string test_file;
|
||||
for (const auto& file : test_files) {
|
||||
if (std::filesystem::exists(file)) {
|
||||
test_file = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (test_file.empty()) {
|
||||
std::cout << "[Test] No test WebM file found. Tried:" << std::endl;
|
||||
for (const auto& file : test_files) {
|
||||
std::cout << " " << file << std::endl;
|
||||
}
|
||||
std::cout << "[Test] Please place a WebM file with AV1 codec in one of these locations." << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TestWebMFileReaderAndAV1Decoder(test_file);
|
||||
}
|
||||
|
||||
} // namespace Vav2Player
|
||||
8
vav2/Vav2Player/Vav2Player/src/TestMain.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace Vav2Player {
|
||||
|
||||
// 통합 테스트 실행 함수
|
||||
bool RunIntegrationTests();
|
||||
|
||||
} // namespace Vav2Player
|
||||