diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..1348f8a
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "external/SDL"]
+ path = external/SDL
+ url = https://github.com/libsdl-org/SDL
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ceba000..2d69a98 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,7 +1,44 @@
-cmake_minimum_required(VERSION 3.5)
+cmake_minimum_required(VERSION 3.18)
project(AVPStudio VERSION 0.2.0 LANGUAGES CXX)
+# Link ffmpeg
+if(WIN32)
+ set(FFMPEG_PATH ${CMAKE_CURRENT_SOURCE_DIR}/external/ffmpeg)
+ set(FFMPEG_VERSION 6.1.1)
+
+ # Download ffmpeg gyan.dev builds from GitHub
+ if(NOT EXISTS ${FFMPEG_PATH})
+ file(DOWNLOAD
+ https://github.com/GyanD/codexffmpeg/releases/download/${FFMPEG_VERSION}/ffmpeg-${FFMPEG_VERSION}-full_build-shared.zip
+ ${CMAKE_CURRENT_SOURCE_DIR}/external/ffmpeg.zip
+ SHOW_PROGRESS
+ )
+ file(ARCHIVE_EXTRACT
+ INPUT ${CMAKE_CURRENT_SOURCE_DIR}/external/ffmpeg.zip
+ DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/external
+ )
+ file(RENAME
+ ${CMAKE_CURRENT_SOURCE_DIR}/external/ffmpeg-${FFMPEG_VERSION}-full_build-shared
+ ${FFMPEG_PATH}
+ )
+ file(REMOVE ${CMAKE_CURRENT_SOURCE_DIR}/external/ffmpeg.zip)
+ endif()
+endif(WIN32)
+
+find_library(LIBAVUTIL_PATH avutil ${FFMPEG_PATH}/lib)
+find_library(LIBAVCODEC_PATH avcodec ${FFMPEG_PATH}/lib)
+find_library(LIBAVFORMAT_PATH avformat ${FFMPEG_PATH}/lib)
+find_library(LIBAVFILTER_PATH avfilter ${FFMPEG_PATH}/lib)
+find_library(LIBSWSCALE_PATH swscale ${FFMPEG_PATH}/lib)
+find_library(LIBSWRESAMPLE_PATH swresample ${FFMPEG_PATH}/lib)
+
+include_directories(${FFMPEG_PATH}/include)
+
+# Link SDL
+add_subdirectory(external/SDL)
+include_directories(external/SDL/include)
+
# Necessary Qt settings
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
@@ -82,20 +119,6 @@ set_target_properties(AVPStudio PROPERTIES
# GNU include
include(GNUInstallDirs)
-# Link ffmpeg
-if(WIN32)
- set(FFMPEG_PATH ${CMAKE_CURRENT_SOURCE_DIR}/external/ffmpeg)
-endif(WIN32)
-
-find_library(LIBAVUTIL_PATH avutil ${FFMPEG_PATH}/lib)
-find_library(LIBAVCODEC_PATH avcodec ${FFMPEG_PATH}/lib)
-find_library(LIBAVFORMAT_PATH avformat ${FFMPEG_PATH}/lib)
-find_library(LIBAVFILTER_PATH avfilter ${FFMPEG_PATH}/lib)
-find_library(LIBSWSCALE_PATH swscale ${FFMPEG_PATH}/lib)
-find_library(LIBSWRESAMPLE_PATH swresample ${FFMPEG_PATH}/lib)
-
-include_directories(${FFMPEG_PATH}/include)
-
# Link libraries
target_link_libraries(AVPStudio PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
@@ -113,7 +136,7 @@ target_link_libraries(AVPStudio PRIVATE
add_subdirectory(tools)
# Set install rules
-install(TARGETS AVPStudio wavgenerator
+install(TARGETS AVPStudio wavgenerator imageorganizer mxlplayer
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
diff --git a/README.md b/README.md
index 0964138..e3383c1 100644
--- a/README.md
+++ b/README.md
@@ -13,11 +13,11 @@ AVPStudio是一个编码工具。它可以将任意视频按照该预设的分
### 素材准备
杜比影院动态视听走廊具有三种不同的规格。三种规格的具体参数如下:
-|规格|尺寸|分辨率|音频轨道|
-|---|---|---|---|
-|Small|5.5m x 2.1m|2830W x 1080H|7.1 PCM Surround|
-|Medium|9m x 2.1m|4633W x 1080H|7.1 PCM Surround|
-|Large|12m x 2.1m|6167W x 1080H|7.1 PCM Surround|
+| 规格 | 尺寸 | 分辨率 | 音频轨道 |
+| ------ | ----------- | ------------- | ---------------- |
+| Small | 5.5m x 2.1m | 2830W x 1080H | 7.1 PCM Surround |
+| Medium | 9m x 2.1m | 4633W x 1080H | 7.1 PCM Surround |
+| Large | 12m x 2.1m | 6167W x 1080H | 7.1 PCM Surround |
有关目标影院的具体规格,可向影院工作人员咨询。若对方无法透露准确信息,可自行对实际投影区域进行测量。
@@ -47,17 +47,32 @@ AVPStudio是一个编码工具。它可以将任意视频按照该预设的分
这样,通过将“![](images/pandorasbox_cue_mark.png)”拉至更远的位置(甚至视频末尾),即可让循环(cue)持续时间更长以避免中途返回开头。
+## 附加工具
+
+### AVPStudio ImageOrganizer
+用于将图片构建为符合杜比影院动态视听走廊分离画面的图片工具。
+
+若您的放映内容仅为单张静态图片,该工具可以省去制作视频的工作。
+
+### AVPStudio WAVGenerator
+用于生成音频WAV的工具。
+
+可搭配ImageOrganizer用于为图片放映内容添加背景音乐,亦可用于及时调整WAV音频的音量大小。
+
+### AVPStudio MXLPlayer
+MXL播放器。
+
+可播放转换完成的(或者官方的)mxl文件预览实际放映效果,亦可将mxl文件转换为H264 MP4视频。
+
## 技术信息
### 原理说明
有关实现的具体原理及画面结构,请参阅[此专栏](https://www.bilibili.com/read/cv27334455/)。
### 构建说明
-截至目前,软件仅在Windows环境下调试并测试通过,尚未针对Linux及macOS环境进行配置。
-
-Windows环境下,请您在开始构建前提前编译ffmpeg库,或在[gyan.dev](https://www.gyan.dev/ffmpeg/builds/)等地下载预构建好的版本。
+截至目前,软件仅在Windows环境下调试并测试通过,尚未针对Linux及macOS环境进行配置与调试。
-在项目中新建```external/ffmpeg```文件夹,并将构建好的ffmpeg库拷贝到该文件夹中。确保其链接库(```.dll.a```;```.lib```等)可以在```external/ffmpeg/lib```下被查找到。
+CMake脚本已被调整为默认从互联网下载预构建ffmpeg。请确保构建时互联网连接畅通。您也可以参阅CMakeLists.txt自行配置外部库。
构建需要完整的Qt6环境。项目必须使用以下Qt库:Qt6Core, Qt6Widgets, Qt6Multimedia, Qt6MultimediaWidgets。
@@ -85,4 +100,12 @@ AVPStudio基于Qt许可证使用Qt6技术。
AVPStudio基于LGPLv2.1及GPLv2使用来自[FFmpeg](https://ffmpeg.org/)的软件。
-AVPStudio是在[GNU GPLv2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html#SEC1)下开放源代码的软件。
\ No newline at end of file
+AVPStudio MXLPlayer基于zlib license使用来自[SDL](https://www.libsdl.org/)的软件。
+
+AVPStudio是在[GNU GPLv2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html#SEC1)下开放源代码的软件。
+
+## 附表 中华人民共和国大陆地区Dolby Cinema部分参数表(2023年7月)
+
+***网友制表,出处见水印。信息仅供参考。***
+
+![](images/cn_dbyc_list_202307.jpg)
\ No newline at end of file
diff --git a/external/SDL b/external/SDL
new file mode 160000
index 0000000..5df737b
--- /dev/null
+++ b/external/SDL
@@ -0,0 +1 @@
+Subproject commit 5df737bb3cd0f110705ee9850c4aa54ba78d08c8
diff --git a/images/cn_dbyc_list_202307.jpg b/images/cn_dbyc_list_202307.jpg
new file mode 100644
index 0000000..16121b0
Binary files /dev/null and b/images/cn_dbyc_list_202307.jpg differ
diff --git a/res/images/close.png b/res/images/close.png
new file mode 100644
index 0000000..a517d4a
Binary files /dev/null and b/res/images/close.png differ
diff --git a/res/images/mute.png b/res/images/mute.png
new file mode 100644
index 0000000..c46d975
Binary files /dev/null and b/res/images/mute.png differ
diff --git a/res/images/volume.png b/res/images/volume.png
new file mode 100644
index 0000000..f69bc7d
Binary files /dev/null and b/res/images/volume.png differ
diff --git a/res/resources.qrc b/res/resources.qrc
index 3cb98a6..57f7454 100644
--- a/res/resources.qrc
+++ b/res/resources.qrc
@@ -21,6 +21,9 @@
images/play.png
images/completed.png
images/error.png
+ images/mute.png
+ images/volume.png
+ images/close.png
texts/aboutinfo_zh_CN.md
diff --git a/res/texts/aboutinfo_zh_CN.md b/res/texts/aboutinfo_zh_CN.md
index 840c8ad..082fd34 100644
--- a/res/texts/aboutinfo_zh_CN.md
+++ b/res/texts/aboutinfo_zh_CN.md
@@ -4,7 +4,7 @@
### 编写者
-[@izwb003](https://space.bilibili.com/36937211) [@筱理_Rize](https://space.bilibili.com/3848521)
+[@izwb003](https://space.bilibili.com/36937211)
### 原理证明
@@ -16,7 +16,7 @@
### 同时提供支持
-[@神奇的红毛丹](https://space.bilibili.com/364856318/) @一个复杂精密的好名字 @寒 @还有这种事? @茶. @R.M.Dolby @Schon @WuChangXD @妙木山蛤蟆仙人
+[@神奇的红毛丹](https://space.bilibili.com/364856318/) [@多真燐](https://space.bilibili.com/8275564) @一个复杂精密的好名字 @寒 @还有这种事? @茶. @R.M.Dolby @Schon @WuChangXD @妙木山蛤蟆仙人
(以上排名不分先后)
@@ -41,6 +41,8 @@ AVPStudio基于Qt许可证使用Qt6技术。
AVPStudio基于LGPLv2.1及GPLv2使用来自[FFmpeg](https://ffmpeg.org/)的软件。它的源代码可以在[GitHub](https://github.com/izwb003/AVPStudio)下载。
+AVPStudio MXLPlayer基于zlib license使用来自[SDL](https://www.libsdl.org/)的软件。
+
### 开放源代码许可
```
diff --git a/src/doprocess.cpp b/src/doprocess.cpp
index a891834..cbe9386 100644
--- a/src/doprocess.cpp
+++ b/src/doprocess.cpp
@@ -15,6 +15,15 @@
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+
+/* To anyone who reads my code:
+ * I am not very familiar with multi-thread programming.
+ * I was sleeping when my teacher taught me these things.
+ * So I may have made some foolish mistakes in this regard, such as using terminate() extensively.
+ * If I have the opportunity to continue maintaining these codes in the future, perhaps I can use more enriched experience to correct them.
+ * But now, they are "usable" - limited to being "able to run", that's all.
+ */
+
#include "doprocess.h"
#include "settings.h"
@@ -41,7 +50,8 @@ static const char *filterGraphLarge =
"[scaled]split[scaled1][scaled2];"
"[scaled1]crop=3840:1080:0:0[left];"
"[scaled2]crop=3840:1080:2327:0[right];"
- "[left][right]vstack=2[out]";
+ "[left][right]vstack=2[ready];"
+ "[ready]fps=24[out]";
static const char *filterGraphMedium =
"[in]pad=iw:ih:0:0:black[expanded];"
"[expanded]scale=4632:1080[scaled];"
@@ -49,7 +59,8 @@ static const char *filterGraphMedium =
"[padded]split[padded1][padded2];"
"[padded1]crop=3840:1080:0:0[left];"
"[padded2]crop=3840:1080:2327:0[right];"
- "[left][right]vstack=2[out]";
+ "[left][right]vstack=2[ready];"
+ "[ready]fps=24[out]";
static const char *filterGraphSmall =
"[in]pad=iw:ih:0:0:black[expanded];"
"[expanded]scale=2830:1080[scaled];"
@@ -57,7 +68,8 @@ static const char *filterGraphSmall =
"[padded]split[padded1][padded2];"
"[padded1]crop=3840:1080:0:0[left];"
"[padded2]crop=3840:1080:2327:0[right];"
- "[left][right]vstack=2[out]";
+ "[left][right]vstack=2[ready];"
+ "[ready]fps=24[out]";
template int toUpperInt(T val)
{
@@ -112,6 +124,7 @@ void TDoProcess::run()
AVFilterInOut *videoFilterOutput = NULL;
AVFilterContext *videoFilterPadCxt = NULL;
+ AVFilterContext *videoFilterFpsCxt = NULL;
const AVFilter *videoFilterSrc = NULL;
AVFilterContext *videoFilterSrcCxt = NULL;
@@ -136,6 +149,8 @@ void TDoProcess::run()
SwrContext *resamplerCxt = NULL;
+ uint64_t audioPTSCounter = 0;
+
// Open input file and find stream info
iVideoFmtCxt = avformat_alloc_context();
avError = avformat_open_input(&iVideoFmtCxt, settings.inputVideoPath.toUtf8(), 0, 0);
@@ -153,7 +168,7 @@ void TDoProcess::run()
// Get input video and audio stream
iVideoStreamID = av_find_best_stream(iVideoFmtCxt, AVMEDIA_TYPE_VIDEO, -1, -1, &iVideoDecoder, 0);
- if(avError < 0)
+ if(iVideoStreamID == AVERROR_STREAM_NOT_FOUND)
{
avErrorMsg = tr("加载输入文件失败:找不到视频流。");
goto end;
@@ -209,6 +224,7 @@ void TDoProcess::run()
oVideoEncoderCxt -> color_trc = settings.outputColor.outputVideoColorTrac;
oVideoEncoderCxt -> profile = 0;
oVideoEncoderCxt -> max_b_frames = 0;
+ oVideoEncoderCxt -> framerate = settings.outputFrameRate;
if(iAudioStreamID != AVERROR_STREAM_NOT_FOUND)
{
@@ -235,7 +251,7 @@ void TDoProcess::run()
goto end;
}
oVideoStream -> time_base = oVideoEncoderCxt->time_base;
- oVideoStream ->r_frame_rate = settings.outputFrameRate;
+ oVideoStream -> r_frame_rate = settings.outputFrameRate;
if(iAudioStreamID != AVERROR_STREAM_NOT_FOUND)
{
@@ -403,6 +419,12 @@ void TDoProcess::run()
}
}
+ if(settings.size == AVP::kAVPMediumSize || settings.size == AVP::kAVPMediumSize)
+ videoFilterFpsCxt = avfilter_graph_get_filter(videoFilterGraph, "Parsed_fps_7");
+ if(settings.size == AVP::kAVPLargeSize)
+ videoFilterFpsCxt = avfilter_graph_get_filter(videoFilterGraph, "Parsed_fps_6");
+ avError = av_opt_set(videoFilterFpsCxt, "fps", QString::number(settings.outputFrameRate.num).toUtf8() + "/" + QString::number(settings.outputFrameRate.den).toUtf8(), AV_OPT_SEARCH_CHILDREN);
+
avError = avfilter_graph_config(videoFilterGraph, 0);
if(avError < 0)
{
@@ -428,17 +450,29 @@ void TDoProcess::run()
// Apply filter
avError = av_buffersrc_add_frame(videoFilterSrcCxt, vFrameIn);
- avError = av_buffersink_get_frame(videoFilterSinkCxt, vFrameFiltered);
+ while(true)
+ {
+ avError = av_buffersink_get_frame(videoFilterSinkCxt, vFrameFiltered);
+ if(avError == AVERROR(EAGAIN) || avError == AVERROR_EOF)
+ break;
- // Rescale to YUV422
- avError = sws_scale_frame(scale422Cxt, vFrameOut, vFrameFiltered);
+ // Rescale to YUV422
+ avError = sws_scale_frame(scale422Cxt, vFrameOut, vFrameFiltered);
- // Encode
- avError = avcodec_send_frame(oVideoEncoderCxt, vFrameOut);
- avError = avcodec_receive_packet(oVideoEncoderCxt, packet);
- av_packet_rescale_ts(packet, oVideoEncoderCxt->time_base, oVideoFmtCxt->streams[0]->time_base);
- avError = av_write_frame(oVideoFmtCxt, packet);
+ // Encode
+ avError = avcodec_send_frame(oVideoEncoderCxt, vFrameOut);
+ avError = avcodec_receive_packet(oVideoEncoderCxt, packet);
+ av_packet_rescale_ts(packet, oVideoEncoderCxt->time_base, oVideoFmtCxt->streams[0]->time_base);
+ avError = av_interleaved_write_frame(oVideoFmtCxt, packet);
+
+ // Unref frame
+ av_frame_unref(vFrameIn);
+ av_frame_unref(vFrameFiltered);
+ av_frame_unref(vFrameOut);
+ }
}
+ // Unref packet
+ av_packet_unref(packet);
}
}
@@ -517,11 +551,21 @@ void TDoProcess::run()
avError = swr_config_frame(resamplerCxt, aFrameOut, aFrameFiltered);
avError = swr_convert_frame(resamplerCxt, aFrameOut, aFrameFiltered);
+ aFrameOut -> pts = audioPTSCounter;
+ audioPTSCounter += oAudioEncoderCxt->frame_size;
+
// Encode
avError = avcodec_send_frame(oAudioEncoderCxt, aFrameOut);
avError = avcodec_receive_packet(oAudioEncoderCxt, packet);
avError = av_write_frame(oAudioFmtCxt, packet);
+
+ // Unref frames
+ av_frame_unref(aFrameIn);
+ av_frame_unref(aFrameFiltered);
+ av_frame_unref(aFrameOut);
}
+ // Unref packet
+ av_packet_unref(packet);
}
}
}
@@ -572,6 +616,11 @@ void TDoProcess::run()
av_frame_free(&vFrameFiltered);
av_frame_free(&vFrameOut);
+ avfilter_free(videoFilterSrcCxt);
+ avfilter_free(videoFilterSinkCxt);
+ avfilter_free(videoFilterPadCxt);
+ avfilter_free(videoFilterFpsCxt);
+
avfilter_graph_free(&videoFilterGraph);
avfilter_inout_free(&videoFilterInput);
avfilter_inout_free(&videoFilterOutput);
@@ -582,6 +631,9 @@ void TDoProcess::run()
av_frame_free(&aFrameFiltered);
av_frame_free(&aFrameOut);
+ avfilter_free(volumeFilterSrcCxt);
+ avfilter_free(volumeFilterSinkCxt);
+ avfilter_free(volumeFilterCxt);
avfilter_graph_free(&volumeFilterGraph);
swr_free(&resamplerCxt);
diff --git a/src/forms/mainwindow/mainwindow.cpp b/src/forms/mainwindow/mainwindow.cpp
index 1427965..e8b7d36 100644
--- a/src/forms/mainwindow/mainwindow.cpp
+++ b/src/forms/mainwindow/mainwindow.cpp
@@ -29,6 +29,7 @@
#include "settings.h"
#include
+#include
#include
MainWindow::MainWindow(QWidget *parent)
@@ -132,11 +133,30 @@ void MainWindow::on_actionWavGenerator_triggered()
delete wavGeneratorProcess;
}
-
-void MainWindow::on_action_triggered()
+void MainWindow::on_actionImageOrganizer_triggered()
{
QProcess *imageOrganizerProcess = new QProcess(this);
imageOrganizerProcess -> startDetached("imageorganizer");
delete imageOrganizerProcess;
}
+
+void MainWindow::on_actionMXLPlayer_triggered()
+{
+ QProcess *mxlPlayerProcess = new QProcess(this);
+ mxlPlayerProcess -> startDetached("mxlplayer");
+ delete mxlPlayerProcess;
+}
+
+
+void MainWindow::on_actionOpenSource_triggered()
+{
+ QDesktopServices::openUrl(QUrl(QString("https://github.com/izwb003/AVPStudio")));
+}
+
+
+void MainWindow::on_actionCheckUpdate_triggered()
+{
+ QDesktopServices::openUrl(QUrl(QString("https://github.com/izwb003/AVPStudio/releases")));
+}
+
diff --git a/src/forms/mainwindow/mainwindow.h b/src/forms/mainwindow/mainwindow.h
index 706395f..3d0d1a0 100644
--- a/src/forms/mainwindow/mainwindow.h
+++ b/src/forms/mainwindow/mainwindow.h
@@ -52,6 +52,9 @@ private slots:
void on_actionNewContent_triggered();
void on_actionOpenFile_triggered();
void on_actionWavGenerator_triggered();
- void on_action_triggered();
+ void on_actionImageOrganizer_triggered();
+ void on_actionMXLPlayer_triggered();
+ void on_actionOpenSource_triggered();
+ void on_actionCheckUpdate_triggered();
};
#endif // MAINWINDOW_H
diff --git a/src/forms/mainwindow/mainwindow.ui b/src/forms/mainwindow/mainwindow.ui
index 97d5fbb..e48184f 100644
--- a/src/forms/mainwindow/mainwindow.ui
+++ b/src/forms/mainwindow/mainwindow.ui
@@ -47,13 +47,16 @@
帮助(&H)
+
+
@@ -95,15 +98,42 @@
true
+
+
+ :/icons/icons/wavgenerator_icon256.ico:/icons/icons/wavgenerator_icon256.ico
+
WAV生成器
-
+
+
+
+ :/icons/icons/imageorganizer_icon256.ico:/icons/icons/imageorganizer_icon256.ico
+
图片排列器
+
+
+
+ :/icons/icons/mxlplayer_icon256.ico:/icons/icons/mxlplayer_icon256.ico
+
+
+ MXL播放器
+
+
+
+
+ 开放源代码
+
+
+
+
+ 检查更新...
+
+
diff --git a/src/forms/mainwindow/pageedit.cpp b/src/forms/mainwindow/pageedit.cpp
index 7b2e32f..69db0dd 100644
--- a/src/forms/mainwindow/pageedit.cpp
+++ b/src/forms/mainwindow/pageedit.cpp
@@ -184,6 +184,13 @@ void PageEdit::on_pushButtonOutput_clicked()
if(settings.outputFilePath == "")
return;
+ if(QFileInfo::exists(settings.outputFilePath + "/" + settings.getOutputVideoFinalName()))
+ if(QMessageBox::question(this, tr("输出文件已存在"), tr("同名视频文件已存在。要覆盖吗?")) == QMessageBox::No)
+ return;
+ if(QFileInfo::exists(settings.outputFilePath + "/" + settings.getOutputAudioFinalName()))
+ if(QMessageBox::question(this, tr("输出文件已存在"), tr("同名音频文件已存在。要覆盖吗?")) == QMessageBox::No)
+ return;
+
emit toProcess();
}
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt
index d92a7c1..aa4ba8f 100644
--- a/tools/CMakeLists.txt
+++ b/tools/CMakeLists.txt
@@ -4,3 +4,4 @@ set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR})
# Add tools
add_subdirectory(wavgenerator)
add_subdirectory(imageorganizer)
+add_subdirectory(mxlplayer)
diff --git a/tools/imageorganizer/src/mainwindow.cpp b/tools/imageorganizer/src/mainwindow.cpp
index e314c09..0609ca2 100644
--- a/tools/imageorganizer/src/mainwindow.cpp
+++ b/tools/imageorganizer/src/mainwindow.cpp
@@ -488,6 +488,10 @@ void MainWindow::doConversion()
av_frame_free(&imageFrameFiltered);
av_frame_free(&imageFrameOut);
+ avfilter_free(imageFilterPadCxt);
+ avfilter_free(imageFilterSrcCxt);
+ avfilter_free(imageFilterSinkCxt);
+
avfilter_graph_free(&imageFilterGraph);
avfilter_inout_free(&imageFilterInput);
avfilter_inout_free(&imageFilterOutput);
diff --git a/tools/mxlplayer/.gitignore b/tools/mxlplayer/.gitignore
new file mode 100644
index 0000000..4a0b530
--- /dev/null
+++ b/tools/mxlplayer/.gitignore
@@ -0,0 +1,74 @@
+# This file is used to ignore files which are generated
+# ----------------------------------------------------------------------------
+
+*~
+*.autosave
+*.a
+*.core
+*.moc
+*.o
+*.obj
+*.orig
+*.rej
+*.so
+*.so.*
+*_pch.h.cpp
+*_resource.rc
+*.qm
+.#*
+*.*#
+core
+!core/
+tags
+.DS_Store
+.directory
+*.debug
+Makefile*
+*.prl
+*.app
+moc_*.cpp
+ui_*.h
+qrc_*.cpp
+Thumbs.db
+*.res
+*.rc
+/.qmake.cache
+/.qmake.stash
+
+# qtcreator generated files
+*.pro.user*
+CMakeLists.txt.user*
+
+# xemacs temporary files
+*.flc
+
+# Vim temporary files
+.*.swp
+
+# Visual Studio generated files
+*.ib_pdb_index
+*.idb
+*.ilk
+*.pdb
+*.sln
+*.suo
+*.vcproj
+*vcproj.*.*.user
+*.ncb
+*.sdf
+*.opensdf
+*.vcxproj
+*vcxproj.*
+
+# MinGW generated files
+*.Debug
+*.Release
+
+# Python byte code
+*.pyc
+
+# Binaries
+# --------
+*.dll
+*.exe
+
diff --git a/tools/mxlplayer/CMakeLists.txt b/tools/mxlplayer/CMakeLists.txt
new file mode 100644
index 0000000..af2977f
--- /dev/null
+++ b/tools/mxlplayer/CMakeLists.txt
@@ -0,0 +1,68 @@
+# Set multi-language TS files
+set(TS_FILES ts/mxlplayer_zh_CN.ts)
+
+# Set project sources
+file(GLOB_RECURSE SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*)
+
+set(PROJECT_SOURCES
+ ${SOURCES}
+ ${TS_FILES}
+ ${CMAKE_SOURCE_DIR}/res/resources.qrc
+)
+
+if(WIN32)
+ configure_file(
+ ${CMAKE_CURRENT_SOURCE_DIR}/res/resources_win.rc.in
+ ${CMAKE_CURRENT_BINARY_DIR}/res/resources_win.rc
+ @ONLY
+ )
+ list(APPEND
+ PROJECT_SOURCES
+ ${CMAKE_CURRENT_BINARY_DIR}/res/resources_win.rc
+ )
+endif(WIN32)
+
+# Set Qt executables
+if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
+ qt_add_executable(mxlplayer
+ MANUAL_FINALIZATION
+ ${PROJECT_SOURCES}
+ )
+ qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
+else()
+ add_executable(mxlplayer
+ ${PROJECT_SOURCES}
+ )
+ qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES})
+endif()
+
+# Link libraries
+target_link_libraries(mxlplayer PRIVATE
+ Qt${QT_VERSION_MAJOR}::Widgets
+ SDL2::SDL2
+ ${LIBAVUTIL_PATH}
+ ${LIBAVFORMAT_PATH}
+ ${LIBAVCODEC_PATH}
+ ${LIBAVFILTER_PATH}
+ ${LIBSWSCALE_PATH}
+ ${LIBSWRESAMPLE_PATH}
+)
+
+# MacOS settings for Qt < 6.1.0 (For Qt > 6.1.0 this is configured automatically)
+if(${QT_VERSION} VERSION_LESS 6.1.0)
+ set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER tech.izwb.AVPStudio.mxlplayer)
+endif()
+
+# Modify Qt executable properties
+set_target_properties(mxlplayer PROPERTIES
+ ${BUNDLE_ID_OPTION}
+ MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
+ MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
+ MACOSX_BUNDLE TRUE
+ WIN32_EXECUTABLE TRUE
+)
+
+# Qt finalize executable
+if(QT_VERSION_MAJOR EQUAL 6)
+ qt_finalize_executable(mxlplayer)
+endif()
diff --git a/tools/mxlplayer/res/resources_win.rc.in b/tools/mxlplayer/res/resources_win.rc.in
new file mode 100644
index 0000000..26989d9
--- /dev/null
+++ b/tools/mxlplayer/res/resources_win.rc.in
@@ -0,0 +1,42 @@
+#include "winver.h"
+
+#define FILE_VERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
+#define FILE_VERSION_STR "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0\0"
+
+#define PRODUCT_VERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,0
+#define PRODUCT_VERSION_STR "@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.0\0"
+
+IDI_ICON1 ICON DISCARDABLE "@CMAKE_SOURCE_DIR@/res/icons/mxlplayer_icon256.ico"
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION FILE_VERSION
+ PRODUCTVERSION PRODUCT_VERSION
+#ifdef DEBUG
+ FILEFLAGS VS_FF_DEBUG
+#else
+ FILEFLAGS VS_FFI_FILEFLAGSMASK
+#endif
+ FILEFLAGSMASK 0x3fL
+ FILEOS VOS_NT_WINDOWS32
+ FILETYPE VFT_APP
+ FILESUBTYPE VFT2_UNKNOWN
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "000004b0"
+ BEGIN
+ VALUE "CompanyName", "izwb003 Productions\0"
+ // VALUE "FileDescription", "AVPStudio MXLPlayer, Dolby Cinema signature entrance (AVP) display content player.\0"
+ VALUE "FileVersion", FILE_VERSION_STR
+ VALUE "InternalName", "mxlplayer.exe\0"
+ VALUE "LegalCopyright", "Copyright (C) 2024 Steven Song (izwb003)\0"
+ VALUE "OriginalFilename", "mxlplayer.exe\0"
+ VALUE "ProductName", "AVP Studio\0"
+ VALUE "ProductVersion", PRODUCT_VERSION_STR
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x0, 1200
+ END
+END
\ No newline at end of file
diff --git a/tools/mxlplayer/src/avpsettings.h b/tools/mxlplayer/src/avpsettings.h
new file mode 100644
index 0000000..8936745
--- /dev/null
+++ b/tools/mxlplayer/src/avpsettings.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#ifndef AVPSETTINGS_H
+#define AVPSETTINGS_H
+
+namespace AVP {
+enum AVPSize {
+ kAVPSmallSize,
+ kAVPMediumSize,
+ kAVPLargeSize
+};
+}
+
+#endif // AVPSETTINGS_H
diff --git a/tools/mxlplayer/src/doexport.cpp b/tools/mxlplayer/src/doexport.cpp
new file mode 100644
index 0000000..c6a3673
--- /dev/null
+++ b/tools/mxlplayer/src/doexport.cpp
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "doexport.h"
+
+#define __STDC_CONSTANT_MACROS
+#define __STDC_FORMAT_MACROS
+
+extern "C" {
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+}
+
+// FFmpeg filter graph description
+static const char *filterGraphLarge =
+ "[in]split[in1][in2];"
+ "[in1]crop=3840:1080:0:0[left];"
+ "[in2]crop=2326:1080:1513:1080[right];"
+ "[left][right]hstack=2[out]";
+static const char *filterGraphMedium =
+ "[in]split[in1][in2];"
+ "[in1]crop=3073:1080:767:0[left];"
+ "[in2]crop=1559:1080:1513:1080[right];"
+ "[left][right]hstack=2[out]";
+static const char *filterGraphSmall =
+ "[in]split[in1][in2];"
+ "[in1]crop=2172:1080:1668:0[left];"
+ "[in2]crop=658:1080:1513:1080[right];"
+ "[left][right]hstack=2[out]";
+
+TDoExport::TDoExport(QObject *parent, QString mxlPath, QString wavPath, QString videoPath, AVP::AVPSize size)
+ : QThread{parent}
+{
+ this->mxlPath = mxlPath;
+ this->wavPath = wavPath;
+ this->videoPath = videoPath;
+ this->size = size;
+}
+
+void TDoExport::run()
+{
+ // FFmpeg init
+ av_log_set_level(AV_LOG_QUIET);
+
+ // Init variables
+ static int avError = 0;
+
+ AVFormatContext *iVideoFmtCxt = NULL;
+ AVFormatContext *iAudioFmtCxt = NULL;
+
+ int iVideoStreamID = -1;
+ int iAudioStreamID = -1;
+
+ const AVCodec *iVideoDecoder = NULL;
+ const AVCodec *iAudioDecoder = NULL;
+ AVCodecContext *iVideoDecoderCxt = NULL;
+ AVCodecContext *iAudioDecoderCxt = NULL;
+
+ const AVCodec *oVideoEncoder = NULL;
+ const AVCodec *oAudioEncoder = NULL;
+ AVCodecContext *oVideoEncoderCxt = NULL;
+ AVCodecContext *oAudioEncoderCxt = NULL;
+
+ AVFormatContext *oVideoFmtCxt = NULL;
+
+ AVStream *oVideoStream = NULL;
+ AVStream *oAudioStream = NULL;
+
+ AVPacket *packet = NULL;
+
+ AVFrame *vFrameIn = NULL;
+ AVFrame *vFrameFiltered = NULL;
+ AVFrame *vFrameOut = NULL;
+
+ AVFilterGraph *videoFilterGraph = NULL;
+ AVFilterInOut *videoFilterInput = NULL;
+ AVFilterInOut *videoFilterOutput = NULL;
+
+ const AVFilter *videoFilterSrc = NULL;
+ AVFilterContext *videoFilterSrcCxt = NULL;
+ const AVFilter *videoFilterSink = NULL;
+ AVFilterContext *videoFilterSinkCxt = NULL;
+
+ SwsContext *scale420Cxt = NULL;
+
+ int videoPTSCounter = 0;
+
+ AVFrame *aFrameIn = NULL;
+ AVFrame *aFrameOut = NULL;
+
+ SwrContext *resamplerCxt = NULL;
+
+ AVAudioFifo *aFifo = NULL;
+ uint8_t **aSamples = NULL;
+ int aSamplesLineSize;
+ uint64_t audioPTSCounter = 0;
+
+ // Open input file and find stream info
+ iVideoFmtCxt = avformat_alloc_context();
+ avError = avformat_open_input(&iVideoFmtCxt, mxlPath.toUtf8(), 0, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL出错"), tr("不能打开输入文件。"));
+ goto end;
+ }
+ avError = avformat_find_stream_info(iVideoFmtCxt, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL出错"), tr("不能找到视频流。"));
+ goto end;
+ }
+ iAudioFmtCxt = avformat_alloc_context();
+ if(!wavPath.isEmpty())
+ {
+ avError = avformat_open_input(&iAudioFmtCxt, wavPath.toUtf8(), 0, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV出错"), tr("不能打开输入文件。"));
+ goto end;
+ }
+ avError = avformat_find_stream_info(iAudioFmtCxt, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV出错"), tr("不能找到音频流。"));
+ goto end;
+ }
+ }
+
+ // Get input video and audio stream
+ iVideoStreamID = av_find_best_stream(iVideoFmtCxt, AVMEDIA_TYPE_VIDEO, -1, -1, &iVideoDecoder, 0);
+ if(iVideoStreamID == AVERROR_STREAM_NOT_FOUND)
+ {
+ emit showError(tr("查找流信息失败"), tr("找不到视频流。"));
+ goto end;
+ }
+ if(!wavPath.isEmpty())
+ {
+ iAudioStreamID = av_find_best_stream(iAudioFmtCxt, AVMEDIA_TYPE_AUDIO, -1, -1, &iAudioDecoder, 0);
+ if(iAudioStreamID == AVERROR_STREAM_NOT_FOUND)
+ {
+ emit showError(tr("查找流信息失败"), tr("找不到音频流。"));
+ goto end;
+ }
+ }
+
+ // Open decoder
+ iVideoDecoderCxt = avcodec_alloc_context3(iVideoDecoder);
+ avError = avcodec_parameters_to_context(iVideoDecoderCxt, iVideoFmtCxt->streams[iVideoStreamID]->codecpar);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL出错"), tr("无法加载解码器。"));
+ goto end;
+ }
+ avError = avcodec_open2(iVideoDecoderCxt, iVideoDecoder, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL出错"), tr("无法打开解码器。"));
+ goto end;
+ }
+
+ if(!wavPath.isEmpty())
+ {
+ iAudioDecoderCxt = avcodec_alloc_context3(iAudioDecoder);
+ avError = avcodec_parameters_to_context(iAudioDecoderCxt, iAudioFmtCxt->streams[iAudioStreamID]->codecpar);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV出错"), tr("无法加载解码器。"));
+ goto end;
+ }
+ avError = avcodec_open2(iAudioDecoderCxt, iAudioDecoder, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV出错"), tr("无法打开解码器。"));
+ goto end;
+ }
+ }
+
+ // Check for video info
+ if(iVideoDecoderCxt->width != 3840 || iVideoDecoderCxt->height != 2160)
+ {
+ emit showError(tr("打开MXL出错"), tr("不支持的视频尺寸。"));
+ goto end;
+ }
+
+ // Init encoder
+ oVideoEncoder = avcodec_find_encoder(AV_CODEC_ID_H264);
+ oVideoEncoderCxt = avcodec_alloc_context3(oVideoEncoder);
+ av_opt_set(oVideoEncoderCxt->priv_data, "preset", "slow", 0);
+ oVideoEncoderCxt -> time_base = av_inv_q(iVideoDecoderCxt->framerate);
+ switch(size)
+ {
+ case AVP::kAVPSmallSize:
+ oVideoEncoderCxt -> width = 2830;
+ break;
+ case AVP::kAVPMediumSize:
+ oVideoEncoderCxt -> width = 4632;
+ break;
+ case AVP::kAVPLargeSize:
+ oVideoEncoderCxt -> width = 6166;
+ break;
+ }
+ oVideoEncoderCxt -> height = 1080;
+ oVideoEncoderCxt -> pix_fmt = AV_PIX_FMT_YUV420P;
+ oVideoEncoderCxt -> gop_size = 10;
+ oVideoEncoderCxt -> max_b_frames = 4;
+ oVideoEncoderCxt -> color_primaries = iVideoDecoderCxt->color_primaries;
+ oVideoEncoderCxt -> color_range = iVideoDecoderCxt->color_range;
+ oVideoEncoderCxt -> color_trc = iVideoDecoderCxt->color_trc;
+ oVideoEncoderCxt -> profile = AV_PROFILE_H264_MAIN;
+
+ if(!wavPath.isEmpty())
+ {
+ oAudioEncoder = avcodec_find_encoder(AV_CODEC_ID_AAC);
+ oAudioEncoderCxt = avcodec_alloc_context3(oAudioEncoder);
+ oAudioEncoderCxt -> time_base = iAudioFmtCxt->streams[iAudioStreamID]->time_base;
+ oAudioEncoderCxt -> ch_layout = iAudioDecoderCxt->ch_layout;
+ oAudioEncoderCxt -> sample_fmt = AV_SAMPLE_FMT_FLTP;
+ oAudioEncoderCxt -> sample_rate = iAudioDecoderCxt -> sample_rate;
+ oAudioEncoderCxt -> profile = FF_PROFILE_AAC_LOW;
+ oAudioEncoderCxt -> flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
+ }
+
+ // Create output format and stream
+ avError = avformat_alloc_output_context2(&oVideoFmtCxt, av_guess_format("mp4", 0, 0), 0, videoPath.toUtf8());
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法创建输出上下文。"));
+ goto end;
+ }
+ oVideoStream = avformat_new_stream(oVideoFmtCxt, 0);
+ avError = avcodec_parameters_from_context(oVideoStream->codecpar, oVideoEncoderCxt);
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法解析输出上下文。"));
+ goto end;
+ }
+ oVideoStream -> time_base = oVideoEncoderCxt->time_base;
+ oVideoStream -> r_frame_rate = iVideoDecoderCxt->framerate;
+
+ if(!wavPath.isEmpty())
+ {
+ oAudioStream = avformat_new_stream(oVideoFmtCxt, 0);
+ avError = avcodec_parameters_from_context(oAudioStream->codecpar, oAudioEncoderCxt);
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法解析输出上下文。"));
+ goto end;
+ }
+ oAudioStream -> time_base = oAudioEncoderCxt->time_base;
+ }
+
+ // Open encoder/file and write file headers
+ avError = avcodec_open2(oVideoEncoderCxt, oVideoEncoder, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法打开视频编码器。"));
+ goto end;
+ }
+ if(!wavPath.isEmpty())
+ {
+ avError = avcodec_open2(oAudioEncoderCxt, oAudioEncoder, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法打开音频编码器。"));
+ goto end;
+ }
+ /*
+ * Special note to this fix:
+ * In newest ffmpeg API, after opening the encoder, we have to copy the parameters again.
+ * Otherwise, decoders will not understand this stream is AAC LC, but a "-1" profile instead.
+ * That's weird and certainly not as we expected, so copy the parameters again to fix.
+ */
+ avError = avcodec_parameters_from_context(oAudioStream->codecpar, oAudioEncoderCxt);
+ }
+
+ avError = avio_open(&oVideoFmtCxt->pb, videoPath.toUtf8(), AVIO_FLAG_WRITE);
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法打开视频输出I/O。"));
+ goto end;
+ }
+ avError = avformat_write_header(oVideoFmtCxt, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法写入视频文件头。"));
+ goto end;
+ }
+
+ // Begin conversion
+ packet = av_packet_alloc();
+
+ // Convert video
+ vFrameIn = av_frame_alloc();
+ vFrameFiltered = av_frame_alloc();
+ vFrameOut = av_frame_alloc();
+
+ emit setProgressText(tr("转换视频中..."));
+ emit setProgressMax(iVideoFmtCxt->streams[iVideoStreamID]->duration * av_q2d(iVideoFmtCxt->streams[iVideoStreamID]->time_base));
+
+ // Set video filter
+ videoFilterGraph = avfilter_graph_alloc();
+
+ videoFilterSrc = avfilter_get_by_name("buffer");
+ char videoFilterSrcArgs[512];
+ snprintf(videoFilterSrcArgs, sizeof(videoFilterSrcArgs), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", iVideoDecoderCxt->width, iVideoDecoderCxt->height, iVideoDecoderCxt->pix_fmt, iVideoFmtCxt->streams[iVideoStreamID]->time_base.num, iVideoFmtCxt->streams[iVideoStreamID]->time_base.den, iVideoDecoderCxt->sample_aspect_ratio.num, iVideoDecoderCxt->sample_aspect_ratio.den);
+ avError = avfilter_graph_create_filter(&videoFilterSrcCxt, videoFilterSrc, "in", videoFilterSrcArgs, 0, videoFilterGraph);
+
+ videoFilterSink = avfilter_get_by_name("buffersink");
+ avError = avfilter_graph_create_filter(&videoFilterSinkCxt, videoFilterSink, "out", 0, 0, videoFilterGraph);
+
+ videoFilterInput = avfilter_inout_alloc();
+ videoFilterInput -> name = av_strdup("in");
+ videoFilterInput -> filter_ctx = videoFilterSrcCxt;
+ videoFilterInput -> pad_idx = 0;
+ videoFilterInput -> next = NULL;
+
+ videoFilterOutput = avfilter_inout_alloc();
+ videoFilterOutput -> name = av_strdup("out");
+ videoFilterOutput -> filter_ctx = videoFilterSinkCxt;
+ videoFilterOutput -> pad_idx = 0;
+ videoFilterOutput -> next = NULL;
+
+ switch(size)
+ {
+ case AVP::kAVPLargeSize:
+ avError = avfilter_graph_parse_ptr(videoFilterGraph, filterGraphLarge, &videoFilterOutput, &videoFilterInput, 0);
+ break;
+ case AVP::kAVPMediumSize:
+ avError = avfilter_graph_parse_ptr(videoFilterGraph, filterGraphMedium, &videoFilterOutput, &videoFilterInput, 0);
+ break;
+ case AVP::kAVPSmallSize:
+ avError = avfilter_graph_parse_ptr(videoFilterGraph, filterGraphSmall, &videoFilterOutput, &videoFilterInput, 0);
+ break;
+ }
+
+ avError = avfilter_graph_config(videoFilterGraph, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("转换出错"), tr("不能创建滤镜链。"));
+ goto end;
+ }
+
+ // Set YUV420 rescaler
+ scale420Cxt = sws_getContext(oVideoEncoderCxt -> width, 1080, iVideoDecoderCxt->pix_fmt, oVideoEncoderCxt -> width, 1080, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, 0, 0, 0);
+
+ while(av_read_frame(iVideoFmtCxt, packet) == 0)
+ {
+ if(packet->stream_index == iVideoStreamID)
+ {
+ avError = avcodec_send_packet(iVideoDecoderCxt, packet);
+ while(true)
+ {
+ avError = avcodec_receive_frame(iVideoDecoderCxt, vFrameIn);
+ if(avError == AVERROR(EAGAIN) || avError == AVERROR_EOF)
+ break;
+
+ emit setProgress(vFrameIn->pkt_dts * av_q2d(iVideoFmtCxt->streams[iVideoStreamID]->time_base));
+
+ // Apply filter
+ avError = av_buffersrc_add_frame(videoFilterSrcCxt, vFrameIn);
+ avError = av_buffersink_get_frame(videoFilterSinkCxt, vFrameFiltered);
+
+ // Rescale to YUV420
+ avError = sws_scale_frame(scale420Cxt, vFrameOut, vFrameFiltered);
+
+ // Encode
+ vFrameOut -> pts = videoPTSCounter ++;
+ avError = avcodec_send_frame(oVideoEncoderCxt, vFrameOut);
+ while(true)
+ {
+ avError = avcodec_receive_packet(oVideoEncoderCxt, packet);
+ if(avError)
+ {
+ av_packet_unref(packet);
+ break;
+ }
+ av_packet_rescale_ts(packet, oVideoEncoderCxt->time_base, oVideoStream->time_base);
+ packet -> stream_index = 0;
+ avError = av_interleaved_write_frame(oVideoFmtCxt, packet);
+ }
+
+ // Unref frames
+ av_frame_unref(vFrameIn);
+ av_frame_unref(vFrameFiltered);
+ av_frame_unref(vFrameOut);
+ }
+ // Unref packet
+ av_packet_unref(packet);
+ }
+ }
+
+ // Flush buffer
+ avcodec_send_frame(oVideoEncoderCxt, NULL);
+ while(true)
+ {
+ avError = avcodec_receive_packet(oVideoEncoderCxt, packet);
+ if(avError)
+ {
+ av_packet_unref(packet);
+ break;
+ }
+ av_packet_rescale_ts(packet, oVideoEncoderCxt->time_base, oVideoStream->time_base);
+ packet -> stream_index = 0;
+ avError = av_interleaved_write_frame(oVideoFmtCxt, packet);
+ }
+
+ // Convert audio
+ if(!wavPath.isEmpty())
+ {
+ aFrameIn = av_frame_alloc();
+ aFrameOut = av_frame_alloc();
+
+ emit setProgressText(tr("转换音频中..."));
+ emit setProgressMax(iAudioFmtCxt->streams[iAudioStreamID]->duration * av_q2d(iAudioFmtCxt->streams[iAudioStreamID]->time_base));
+ emit setProgress(0);
+
+ // Set resampler
+ avError = swr_alloc_set_opts2(&resamplerCxt, &iAudioDecoderCxt->ch_layout, AV_SAMPLE_FMT_FLTP, iAudioDecoderCxt->sample_rate, &iAudioDecoderCxt->ch_layout, iAudioDecoderCxt->sample_fmt, iAudioDecoderCxt->sample_rate, 0, 0);
+ avError = swr_init(resamplerCxt);
+
+ // Set FIFO
+ /* Special note to this fix:
+ * AAC LC requires 1024 samples to be fed into the encoder at a time.
+ * But the number of samples in a frame after the sample often does not reach this number.
+ * Therefore, it is necessary to maintain a FIFO queue to ensure that the number of samples sent to the encoder each time is 1024.
+ */
+ aFifo = av_audio_fifo_alloc(AV_SAMPLE_FMT_FLTP, iAudioDecoderCxt->ch_layout.nb_channels, 1);
+
+ while(av_read_frame(iAudioFmtCxt, packet) == 0)
+ {
+ if(packet->stream_index == iAudioStreamID)
+ {
+ avError = avcodec_send_packet(iAudioDecoderCxt, packet);
+ while(true)
+ {
+ avError = avcodec_receive_frame(iAudioDecoderCxt, aFrameIn);
+ if(avError == AVERROR(EAGAIN) || avError == AVERROR_EOF)
+ break;
+
+ emit setProgress(aFrameIn->pkt_dts * av_q2d(iAudioFmtCxt->streams[iAudioStreamID]->time_base));
+
+ // Resample
+ avError = av_samples_alloc_array_and_samples(&aSamples, &aSamplesLineSize, iAudioDecoderCxt->ch_layout.nb_channels, aFrameIn->nb_samples, AV_SAMPLE_FMT_FLTP, 0);
+ avError = swr_convert(resamplerCxt, aSamples, aFrameIn->nb_samples, (const uint8_t**)aFrameIn->extended_data, aFrameIn->nb_samples);
+
+ // Organize FIFO
+ avError = av_audio_fifo_write(aFifo, (void **)aSamples, aFrameIn->nb_samples);
+
+ // Encode
+ while(av_audio_fifo_size(aFifo) >= oAudioEncoderCxt->frame_size)
+ {
+ // Copy frame settings
+ av_frame_unref(aFrameOut);
+ aFrameOut -> ch_layout = aFrameIn -> ch_layout;
+ aFrameOut -> sample_rate = aFrameIn -> sample_rate;
+ aFrameOut -> format = AV_SAMPLE_FMT_FLTP;
+ aFrameOut -> nb_samples = oAudioEncoderCxt->frame_size;
+ aFrameOut -> pts = audioPTSCounter;
+ audioPTSCounter += aFrameOut->nb_samples;
+ avError = av_frame_get_buffer(aFrameOut, 0);
+
+ // Encoding
+ avError = av_audio_fifo_read(aFifo, (void **)aFrameOut->data, oAudioEncoderCxt->frame_size);
+ avError = avcodec_send_frame(oAudioEncoderCxt, aFrameOut);
+ while(true)
+ {
+ avError = avcodec_receive_packet(oAudioEncoderCxt, packet);
+ if(avError == AVERROR(EAGAIN) || avError == AVERROR_EOF)
+ break;
+ packet -> stream_index = 1;
+ avError = av_write_frame(oVideoFmtCxt, packet);
+ }
+ }
+ }
+ }
+ }
+
+ // Flush buffer
+
+ // Copy frame settings
+ av_frame_unref(aFrameOut);
+ aFrameOut -> ch_layout = aFrameIn -> ch_layout;
+ aFrameOut -> sample_rate = aFrameIn -> sample_rate;
+ aFrameOut -> format = AV_SAMPLE_FMT_FLTP;
+ aFrameOut -> nb_samples = av_audio_fifo_size(aFifo);
+ aFrameOut -> pts = audioPTSCounter;
+ avError = av_frame_get_buffer(aFrameOut, 0);
+
+ // Encoding
+ avError = av_audio_fifo_read(aFifo, (void **)aFrameOut->data, oAudioEncoderCxt->frame_size);
+ avError = avcodec_send_frame(oAudioEncoderCxt, aFrameOut);
+ while(true)
+ {
+ avError = avcodec_receive_packet(oAudioEncoderCxt, packet);
+ if(avError == AVERROR(EAGAIN) || avError == AVERROR_EOF)
+ break;
+ packet -> stream_index = 1;
+ avError = av_write_frame(oVideoFmtCxt, packet);
+ }
+ }
+
+ // Write file tail
+ avError = av_write_trailer(oVideoFmtCxt);
+ if(avError < 0)
+ {
+ emit showError(tr("写入输出视频失败"), tr("无法写入视频文件尾。"));
+ goto end;
+ }
+
+ // Close files
+ avformat_close_input(&iVideoFmtCxt);
+ if(!wavPath.isEmpty())
+ avformat_close_input(&iAudioFmtCxt);
+ avio_close(oVideoFmtCxt->pb);
+
+ avError = 0;
+
+end: // Jump flag for errors
+ avformat_free_context(iVideoFmtCxt);
+ avformat_free_context(iAudioFmtCxt);
+
+ avcodec_free_context(&iVideoDecoderCxt);
+ if(!wavPath.isEmpty())
+ avcodec_free_context(&iAudioDecoderCxt);
+
+ avcodec_free_context(&oVideoEncoderCxt);
+ if(!wavPath.isEmpty())
+ avcodec_free_context(&oAudioEncoderCxt);
+
+ avformat_free_context(oVideoFmtCxt);
+
+ av_packet_free(&packet);
+
+ av_frame_free(&vFrameIn);
+ av_frame_free(&vFrameFiltered);
+ av_frame_free(&vFrameOut);
+
+ avfilter_free(videoFilterSrcCxt);
+ avfilter_free(videoFilterSinkCxt);
+
+ avfilter_graph_free(&videoFilterGraph);
+ avfilter_inout_free(&videoFilterInput);
+ avfilter_inout_free(&videoFilterOutput);
+
+ sws_freeContext(scale420Cxt);
+
+ if(!wavPath.isEmpty())
+ {
+ av_frame_free(&aFrameIn);
+ av_frame_free(&aFrameOut);
+
+ swr_free(&resamplerCxt);
+
+ av_audio_fifo_free(aFifo);
+ av_freep(aSamples);
+ }
+
+ if(avError == 0)
+ emit completed();
+}
diff --git a/tools/mxlplayer/src/doexport.h b/tools/mxlplayer/src/doexport.h
new file mode 100644
index 0000000..395b197
--- /dev/null
+++ b/tools/mxlplayer/src/doexport.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#ifndef TDOEXPORT_H
+#define TDOEXPORT_H
+
+#include "avpsettings.h"
+
+#include
+
+class TDoExport : public QThread
+{
+ Q_OBJECT
+public:
+ explicit TDoExport(QObject *parent = nullptr, QString mxlPath = "", QString wavPath = "", QString videoPath = "", AVP::AVPSize size = AVP::kAVPMediumSize);
+
+signals:
+ void showError(QString errorTitle, QString errorMsg);
+
+ void setProgressText(QString text);
+
+ void setProgressMax(int val);
+
+ void setProgress(int val);
+
+ void completed();
+
+protected:
+ void run();
+
+private:
+ QString mxlPath = "";
+ QString wavPath = "";
+ QString videoPath = "";
+ AVP::AVPSize size = AVP::kAVPMediumSize;
+};
+
+#endif // TDOEXPORT_H
diff --git a/tools/mxlplayer/src/main.cpp b/tools/mxlplayer/src/main.cpp
new file mode 100644
index 0000000..77aed24
--- /dev/null
+++ b/tools/mxlplayer/src/main.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "mainwindow.h"
+
+#include
+#include
+#include
+#include
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+
+ QTranslator translator;
+ const QStringList uiLanguages = QLocale::system().uiLanguages();
+ for (const QString &locale : uiLanguages) {
+ const QString baseName = "mxlplayer_" + QLocale(locale).name();
+ if (translator.load(":/i18n/" + baseName)) {
+ a.installTranslator(&translator);
+ break;
+ }
+ }
+
+ // Set style
+ QFile styleSheetFile(":/styles/styles/mainstyle.qss");
+ styleSheetFile.open(QFile::ReadOnly);
+ QString styleSheet = QLatin1String(styleSheetFile.readAll());
+ qApp->setStyleSheet(styleSheet);
+ styleSheetFile.close();
+
+ MainWindow w;
+ w.show();
+ return a.exec();
+}
diff --git a/tools/mxlplayer/src/mainwindow.cpp b/tools/mxlplayer/src/mainwindow.cpp
new file mode 100644
index 0000000..230ff0d
--- /dev/null
+++ b/tools/mxlplayer/src/mainwindow.cpp
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "mainwindow.h"
+#include "./ui_mainwindow.h"
+
+#include "playcontrol.h"
+
+#include
+#include
+#include
+#include
+
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::MainWindow)
+{
+ ui->setupUi(this);
+
+ this->setWindowFlags(windowFlags()& ~Qt::WindowMaximizeButtonHint);
+ this->setFixedSize(this->width(), this->height());
+}
+
+MainWindow::~MainWindow()
+{
+ delete ui;
+}
+
+void MainWindow::do_showError(QString errorTitle, QString errorMsg)
+{
+ progressDialog -> close();
+ QMessageBox::critical(this, errorTitle, errorMsg);
+}
+
+void MainWindow::do_completed()
+{
+ progressDialog->close();
+ QMessageBox::information(this, tr("MP4导出"), tr("导出视频完成。"));
+ delete progressDialog;
+ progressDialog = nullptr;
+}
+
+void MainWindow::do_canceled()
+{
+ progressDialog->close();
+ doExport->terminate();
+ delete progressDialog;
+ progressDialog = nullptr;
+}
+
+void MainWindow::on_pushButtonExportMP4_clicked()
+{
+ QFileInfo mxlInfo(ui->lineEditMXLPath->text());
+ if(!mxlInfo.exists())
+ {
+ QMessageBox::critical(this, tr("载入文件出错"), tr("无法载入MXL文件。"));
+ return;
+ }
+
+ QFileInfo wavInfo(ui->lineEditWAVPath->text());
+ if(!wavInfo.exists())
+ {
+ if(QMessageBox::question(this, tr("载入文件出错"), tr("无法打开WAV文件,输出文件将没有音频。\n要继续吗?")) == QMessageBox::No)
+ return;
+ else
+ ui->lineEditWAVPath->setText("");
+ }
+
+ QString videoPath = QFileDialog::getSaveFileName(this, tr("选择保存MP4文件位置"), QDir::homePath(), tr("H264 MP4视频 (*.mp4)"));
+
+ AVP::AVPSize size = getSize();
+
+ doExport = new TDoExport(this, ui->lineEditMXLPath->text(), ui->lineEditWAVPath->text(), videoPath, size);
+ connect(doExport, SIGNAL(showError(QString,QString)), this, SLOT(do_showError(QString,QString)));
+ connect(doExport, SIGNAL(completed()), this, SLOT(do_completed()));
+ connect(doExport, &QThread::finished, doExport, &QObject::deleteLater);
+
+ progressDialog = new QProgressDialog(tr("导出视频文件..."), tr("取消"), 0, 0, this);
+ progressDialogBar = new QProgressBar(progressDialog);
+ progressDialogBar->setTextVisible(false);
+ progressDialog->setWindowTitle(tr("MP4导出"));
+ progressDialog->setWindowModality(Qt::WindowModal);
+ progressDialog->setAutoReset(false);
+ progressDialog->setAutoClose(false);
+ progressDialog->setFixedSize(300, 100);
+ progressDialog->setBar(progressDialogBar);
+ progressDialog->show();
+ connect(doExport, SIGNAL(setProgressText(QString)), progressDialog, SLOT(setLabelText(QString)));
+ connect(doExport, SIGNAL(setProgressMax(int)), progressDialog, SLOT(setMaximum(int)));
+ connect(doExport, SIGNAL(setProgress(int)), progressDialog, SLOT(setValue(int)));
+ connect(progressDialog, SIGNAL(canceled()), this, SLOT(do_canceled()));
+
+ doExport->start();
+}
+
+AVP::AVPSize MainWindow::getSize()
+{
+ if(ui->radioButtonSmall->isChecked())
+ return AVP::kAVPSmallSize;
+ else if(ui->radioButtonMedium->isChecked())
+ return AVP::kAVPMediumSize;
+ else if(ui->radioButtonLarge->isChecked())
+ return AVP::kAVPLargeSize;
+ return AVP::kAVPMediumSize;
+}
+
+
+void MainWindow::on_pushButtonMXLBrowse_clicked()
+{
+ ui->lineEditMXLPath->setText(QFileDialog::getOpenFileName(this, tr("打开MXL文件"), QDir::homePath(), "Christie PandorasBox MPEG Video (*.mxl)"));
+}
+
+
+void MainWindow::on_pushButtonWAVBrowse_clicked()
+{
+ ui->lineEditWAVPath->setText(QFileDialog::getOpenFileName(this, tr("打开WAV文件"), QDir::homePath(), "WAVE Audio (*.wav)"));
+}
+
+
+void MainWindow::on_pushButtonPlay_clicked()
+{
+ PlayControl *playControl = new PlayControl(this, ui->lineEditMXLPath->text(), ui->lineEditWAVPath->text(), getSize());
+
+ playControl->setParent(NULL);
+ playControl->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);
+
+ playControl->show();
+ this->hide();
+}
+
diff --git a/tools/mxlplayer/src/mainwindow.h b/tools/mxlplayer/src/mainwindow.h
new file mode 100644
index 0000000..a89692e
--- /dev/null
+++ b/tools/mxlplayer/src/mainwindow.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include
+#include
+#include
+
+#include "doexport.h"
+
+QT_BEGIN_NAMESPACE
+namespace Ui {
+class MainWindow;
+}
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(QWidget *parent = nullptr);
+ ~MainWindow();
+
+private slots:
+ void do_showError(QString errorTitle, QString errorMsg);
+
+ void do_completed();
+
+ void do_canceled();
+
+ void on_pushButtonExportMP4_clicked();
+
+ void on_pushButtonMXLBrowse_clicked();
+
+ void on_pushButtonWAVBrowse_clicked();
+
+ void on_pushButtonPlay_clicked();
+
+private:
+ Ui::MainWindow *ui;
+
+ TDoExport *doExport;
+
+ QProgressBar *progressDialogBar;
+ QProgressDialog *progressDialog;
+
+ AVP::AVPSize getSize();
+};
+#endif // MAINWINDOW_H
diff --git a/tools/mxlplayer/src/mainwindow.ui b/tools/mxlplayer/src/mainwindow.ui
new file mode 100644
index 0000000..09d045c
--- /dev/null
+++ b/tools/mxlplayer/src/mainwindow.ui
@@ -0,0 +1,126 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 491
+ 199
+
+
+
+ AVPStudio MXLPlayer - Home
+
+
+
+ -
+
+
-
+
+
+ MXL文件:
+
+
+
+ -
+
+
+ -
+
+
+ 浏览...
+
+
+
+
+
+ -
+
+
-
+
+
+ WAV文件:
+
+
+
+ -
+
+
+ -
+
+
+ 浏览...
+
+
+
+
+
+ -
+
+
+ 尺寸选择
+
+
+
-
+
+
+ Small - 5.5M
+
+
+
+ -
+
+
+ Medium - 9M
+
+
+ true
+
+
+
+ -
+
+
+ Large - 12M
+
+
+
+
+
+
+ -
+
+
-
+
+
+ 播放...
+
+
+
+ -
+
+
+ 导出MP4...
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/mxlplayer/src/playcontrol.cpp b/tools/mxlplayer/src/playcontrol.cpp
new file mode 100644
index 0000000..92f3ef6
--- /dev/null
+++ b/tools/mxlplayer/src/playcontrol.cpp
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "playcontrol.h"
+#include "ui_playcontrol.h"
+
+#include
+#include
+
+PlayControl::PlayControl(QWidget *parent, QString mxlPath, QString wavPath, AVP::AVPSize size)
+ : QWidget(parent)
+ , ui(new Ui::PlayControl)
+{
+ this -> mxlPath = mxlPath;
+ this -> wavPath = wavPath;
+ this -> size = size;
+ this -> mainPage = qobject_cast(parent);
+
+ ui->setupUi(this);
+
+ this->setGeometry((qApp->primaryScreen()->size().width() / 2) - (this->width() / 2), (qApp->primaryScreen()->size().height() / 6) * 5 - (this->height() / 2), this->width(), this->height());
+
+ setMouseTracking(true);
+ ui->labelIcon->installEventFilter(this);
+
+ videoPlayer = new TPlayVideo(this, mxlPath, wavPath, size);
+ connect(videoPlayer, SIGNAL(showError(QString)), this, SLOT(do_showError(QString)));
+ connect(videoPlayer, SIGNAL(setPositionBarMax(int,double)), this, SLOT(do_setPositionBarMax(int,double)));
+ connect(videoPlayer, SIGNAL(setPosition(int)), this, SLOT(do_setPosition(int)));
+ connect(videoPlayer, SIGNAL(sdlQuit()), this, SLOT(on_toolButtonBack_clicked()));
+ connect(this, SIGNAL(play()), videoPlayer, SLOT(do_play()));
+ connect(this, SIGNAL(pause()), videoPlayer, SLOT(do_pause()));
+ connect(this, SIGNAL(updatePosition(int)), videoPlayer, SLOT(do_updatePosition(int)));
+
+ if(videoPlayer->init())
+ qApp->quit();
+
+ videoPlayer->start();
+}
+
+PlayControl::~PlayControl()
+{
+ delete ui;
+}
+
+void PlayControl::do_showError(QString errorMsg)
+{
+ QMessageBox::critical(this, tr("错误"), errorMsg);
+}
+
+void PlayControl::do_setPositionBarMax(int val, double timebase)
+{
+ ui->horizontalSliderPosition->setMaximum(val);
+ this->timebase = timebase;
+ QTime totalTime(0, 0, 0);
+ totalTime = totalTime.addSecs((int)((double)val * timebase));
+ ui->labelTotalTime->setText(totalTime.toString("mm:ss"));
+}
+
+void PlayControl::do_setPosition(int val)
+{
+ if(!ui->horizontalSliderPosition->isSliderDown())
+ ui->horizontalSliderPosition->setValue(val);
+ QTime time(0, 0, 0);
+ time = time.addSecs((int)((double)val * timebase));
+ ui->labelPlayTime->setText(time.toString("mm:ss"));
+}
+
+void PlayControl::mousePressEvent(QMouseEvent *event)
+{
+ if(isDragging)
+ lastMousePos = event->globalPosition().toPoint() - frameGeometry().topLeft();
+ event->accept();
+}
+
+void PlayControl::mouseMoveEvent(QMouseEvent *event)
+{
+ if(isDragging)
+ move(event->globalPosition().toPoint() - lastMousePos);
+ event->accept();
+}
+
+void PlayControl::mouseReleaseEvent(QMouseEvent *event)
+{
+ if(isDragging)
+ isDragging = false;
+ event->accept();
+}
+
+bool PlayControl::eventFilter(QObject *object, QEvent *event)
+{
+ if(object == ui->labelIcon)
+ {
+ if(event->type() == QEvent::MouseButtonPress)
+ startDragging();
+ else if(event->type() == QEvent::MouseButtonRelease)
+ stopDragging();
+ return false;
+ }
+ return QWidget::eventFilter(object, event);
+}
+
+
+void PlayControl::on_toolButtonBack_clicked()
+{
+ qApp->quit();
+}
+
+
+void PlayControl::on_toolButtonMute_clicked(bool checked)
+{
+ if(checked)
+ {
+ ui->toolButtonMute->setIcon(QIcon(":/images/images/mute.png"));
+ muteVolume = ui->horizontalSliderVolume->value();
+ ui->horizontalSliderVolume->setValue(0);
+ }
+ else
+ {
+ ui->toolButtonMute->setIcon(QIcon(":/images/images/volume.png"));
+ ui->horizontalSliderVolume->setValue(muteVolume);
+ }
+}
+
+
+void PlayControl::on_horizontalSliderVolume_valueChanged(int value)
+{
+ QPoint globalPos = ui->horizontalSliderVolume->mapToGlobal(QPoint(0, 0));
+ QToolTip::showText(QPoint(globalPos.x() + ui->horizontalSliderVolume->width() / 2, globalPos.y()), QString::number(ui->horizontalSliderVolume->value()));
+}
+
+
+void PlayControl::on_toolButtonPlayPause_clicked(bool checked)
+{
+ if(checked)
+ {
+ ui->toolButtonPlayPause->setIcon(QIcon(":/images/images/pause.png"));
+ emit play();
+ }
+ else
+ {
+ ui->toolButtonPlayPause->setIcon(QIcon(":/images/images/play.png"));
+ emit pause();
+ }
+}
+
+
+void PlayControl::on_horizontalSliderPosition_sliderReleased()
+{
+ emit updatePosition(ui->horizontalSliderPosition->value());
+}
diff --git a/tools/mxlplayer/src/playcontrol.h b/tools/mxlplayer/src/playcontrol.h
new file mode 100644
index 0000000..e38141c
--- /dev/null
+++ b/tools/mxlplayer/src/playcontrol.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#ifndef PLAYCONTROL_H
+#define PLAYCONTROL_H
+
+#include "mainwindow.h"
+#include "playvideo.h"
+#include "avpsettings.h"
+
+#include
+#include
+#include
+
+namespace Ui {
+class PlayControl;
+}
+
+class PlayControl : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit PlayControl(QWidget *parent = nullptr, QString mxlPath = "", QString wavPath = "", AVP::AVPSize size = AVP::kAVPMediumSize);
+ ~PlayControl();
+
+signals:
+ void updatePosition(int val);
+
+ void play();
+
+ void pause();
+
+private:
+ Ui::PlayControl *ui;
+
+ MainWindow *mainPage;
+
+ QString mxlPath = "";
+ QString wavPath = "";
+ AVP::AVPSize size = AVP::kAVPMediumSize;
+
+ bool isDragging = false;
+ QPoint lastMousePos;
+
+ double timebase = 0;
+
+ int muteVolume = 0;
+
+ TPlayVideo *videoPlayer = NULL;
+
+private slots:
+ inline void startDragging() {isDragging = true;}
+ inline void stopDragging() {isDragging = false;}
+
+ void do_showError(QString errorMsg);
+
+ void do_setPositionBarMax(int val, double timebase);
+
+ void do_setPosition(int val);
+
+ void on_toolButtonBack_clicked();
+
+ void on_toolButtonMute_clicked(bool checked);
+
+ void on_horizontalSliderVolume_valueChanged(int value);
+
+ void on_toolButtonPlayPause_clicked(bool checked);
+
+ void on_horizontalSliderPosition_sliderReleased();
+
+protected:
+ void mousePressEvent(QMouseEvent *event) override;
+ void mouseMoveEvent(QMouseEvent *event) override;
+ void mouseReleaseEvent(QMouseEvent *event) override;
+ bool eventFilter(QObject *object, QEvent *event) override;
+};
+
+#endif // PLAYCONTROL_H
diff --git a/tools/mxlplayer/src/playcontrol.ui b/tools/mxlplayer/src/playcontrol.ui
new file mode 100644
index 0000000..c72a968
--- /dev/null
+++ b/tools/mxlplayer/src/playcontrol.ui
@@ -0,0 +1,157 @@
+
+
+ PlayControl
+
+
+
+ 0
+ 0
+ 959
+ 41
+
+
+
+ 播放控制
+
+
+ #PlayControl
+{
+ border-radius: 5px;
+}
+
+
+ -
+
+
+
+ 20
+ 20
+
+
+
+
+ 20
+ 20
+
+
+
+
+
+
+ :/icons/icons/mxlplayer_icon256.ico
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ 00:00
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ 00:00
+
+
+
+ -
+
+
+ 播放/暂停
+
+
+
+
+
+
+ :/images/images/play.png:/images/images/play.png
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ 静音
+
+
+
+
+
+
+ :/images/images/volume.png:/images/images/volume.png
+
+
+ true
+
+
+
+ -
+
+
+ 100
+
+
+ 50
+
+
+ Qt::Horizontal
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ 退出
+
+
+
+
+
+
+ :/images/images/close.png:/images/images/close.png
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/mxlplayer/src/playvideo.cpp b/tools/mxlplayer/src/playvideo.cpp
new file mode 100644
index 0000000..8b49cba
--- /dev/null
+++ b/tools/mxlplayer/src/playvideo.cpp
@@ -0,0 +1,562 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "playvideo.h"
+
+#include
+
+#include
+
+#define __STDC_CONSTANT_MACROS
+#define __STDC_FORMAT_MACROS
+
+extern "C" {
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+}
+
+#define SDL_CUSTOM_REFRESH_EVENT (SDL_USEREVENT + 1)
+#define SDL_CUSTOM_POSITION_UPDATE_EVENT (SDL_USEREVENT + 2)
+#define SDL_CUSTOM_QUIT_EVENT (SDL_USEREVENT + 4)
+
+#define MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio
+
+/*
+ * Flag to control the behavior of the SDL refresher.
+ * case 0: Play normally.
+ * case 1: Paused.
+ * case 2: Quit.
+ */
+static int refresherFlag = 1;
+
+// FFmpeg filter graph description
+static const char *filterGraphLarge =
+ "[in]split[in1][in2];"
+ "[in1]crop=3840:1080:0:0[left];"
+ "[in2]crop=2326:1080:1513:1080[right];"
+ "[left][right]hstack=2[out]";
+static const char *filterGraphMedium =
+ "[in]split[in1][in2];"
+ "[in1]crop=3073:1080:767:0[left];"
+ "[in2]crop=1559:1080:1513:1080[right];"
+ "[left][right]hstack=2[out]";
+static const char *filterGraphSmall =
+ "[in]split[in1][in2];"
+ "[in1]crop=2172:1080:1668:0[left];"
+ "[in2]crop=658:1080:1513:1080[right];"
+ "[left][right]hstack=2[out]";
+
+static AVFormatContext *videoFmtCxt = NULL;
+static int videoStreamID = 0;
+static AVFormatContext *audioFmtCxt = NULL;
+static int audioStreamID = 0;
+
+static const AVCodec *videoDecoder = NULL;
+static AVCodecContext *videoDecoderCxt = NULL;
+static const AVCodec *audioDecoder = NULL;
+static AVCodecContext *audioDecoderCxt = NULL;
+
+static AVFilterGraph *videoFilterGraph = NULL;
+static AVFilterInOut *videoFilterInput = NULL;
+static AVFilterInOut *videoFilterOutput = NULL;
+
+static const AVFilter *videoFilterSrc = NULL;
+static AVFilterContext *videoFilterSrcCxt = NULL;
+static const AVFilter *videoFilterSink = NULL;
+static AVFilterContext *videoFilterSinkCxt = NULL;
+
+static SwsContext *scalerCxt = NULL;
+static SwrContext *resamplerCxt = NULL;
+
+static AVPacket *vPacket = NULL;
+static AVPacket *aPacket = NULL;
+static AVFrame *frameIn = NULL;
+static AVFrame *frameFiltered = NULL;
+static AVFrame *frameScaled = NULL;
+static AVFrame *frame = NULL;
+
+static int iAudioBufferSize = 0;
+static int iAudioBufferSampleCount = 0;
+static uint8_t *iAudioBuffer = NULL;
+static int oAudioBufferSize = 0;
+static int oAudioBufferSampleCount = 0;
+static uint8_t *oAudioBuffer = NULL;
+
+AVAudioFifo *audioQueue = NULL;
+SDL_mutex *audioQueueMutex = NULL;
+
+static SDL_Window *window = NULL;
+static SDL_Renderer *renderer = NULL;
+static SDL_Texture *texture = NULL;
+static SDL_Thread *threadRefresh = NULL;
+static SDL_Thread *threadDecodeAudio = NULL;
+static SDL_Event eventSDL;
+static SDL_AudioSpec wantedSpec;
+
+static int SDLRefresher(void *opaque)
+{
+ SDL_Event refreshEvent;
+ refreshEvent.type = SDL_CUSTOM_REFRESH_EVENT;
+ while(true)
+ {
+ if(refresherFlag == 0)
+ {
+ SDL_PushEvent(&refreshEvent);
+ SDL_Delay(*(int*)opaque);
+ }
+ else if(refresherFlag == 1)
+ continue;
+ else if(refresherFlag == 2)
+ break;
+ }
+ return 0;
+}
+
+static int SDLAudioDecoder(void *opaque)
+{
+ static int avError = 0;
+
+ while(true)
+ {
+ if(refresherFlag != 0)
+ continue;
+
+ if(av_read_frame(audioFmtCxt, aPacket) == 0)
+ {
+ if(aPacket->stream_index == audioStreamID)
+ {
+ avError = avcodec_send_packet(audioDecoderCxt, aPacket);
+ while(true)
+ {
+ avError = avcodec_receive_frame(audioDecoderCxt, frame);
+ if(avError == AVERROR(EAGAIN) || avError == AVERROR_EOF)
+ break;
+
+ iAudioBufferSampleCount = swr_convert(resamplerCxt, &iAudioBuffer, MAX_AUDIO_FRAME_SIZE, (const uint8_t**)frame->data, frame->nb_samples);
+
+ iAudioBufferSize = av_samples_get_buffer_size(0, frame->ch_layout.nb_channels, iAudioBufferSampleCount, AV_SAMPLE_FMT_S16, 1);
+
+ while(iAudioBufferSampleCount > av_audio_fifo_space(audioQueue));
+ SDL_LockMutex(audioQueueMutex);
+ avError = av_audio_fifo_write(audioQueue, (void**)&iAudioBuffer, iAudioBufferSampleCount);
+ SDL_UnlockMutex(audioQueueMutex);
+
+ av_frame_unref(frame);
+ }
+ av_packet_unref(aPacket);
+ }
+ }
+ else
+ SDL_PauseAudio(1);
+ }
+
+ return 0;
+}
+
+static void SDLFillAudio(void *data, uint8_t *stream, int length)
+{
+ SDL_memset(stream, 0, length);
+ SDL_LockMutex(audioQueueMutex);
+ oAudioBufferSampleCount = av_audio_fifo_read(audioQueue, (void**)&oAudioBuffer, length / 4);
+ SDL_UnlockMutex(audioQueueMutex);
+ if(oAudioBufferSampleCount < 0)
+ return;
+ oAudioBufferSize = av_samples_get_buffer_size(0, audioDecoderCxt->ch_layout.nb_channels, oAudioBufferSampleCount, AV_SAMPLE_FMT_S16, 1);
+ SDL_MixAudio(stream, oAudioBuffer, oAudioBufferSize, SDL_MIX_MAXVOLUME);
+}
+
+TPlayVideo::TPlayVideo(QObject *parent, QString mxlPath, QString wavPath, AVP::AVPSize size)
+ : QThread{parent}
+{
+ this->mxlPath = mxlPath;
+ this->wavPath = wavPath;
+ this->size = size;
+
+ AVPHeight = 1080;
+ switch(size)
+ {
+ case AVP::kAVPLargeSize:
+ AVPWidth = 6166;
+ break;
+ case AVP::kAVPMediumSize:
+ AVPWidth = 4632;
+ break;
+ case AVP::kAVPSmallSize:
+ AVPWidth = 2830;
+ break;
+ }
+}
+
+int TPlayVideo::init()
+{
+ // FFmpeg init
+ av_log_set_level(AV_LOG_QUIET);
+ static int avError = 0;
+
+ // Open input file and get stream info
+ avError = avformat_open_input(&videoFmtCxt, mxlPath.toUtf8(), 0, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL失败。"));
+ cleanup();
+ return avError;
+ }
+ avError = avformat_find_stream_info(videoFmtCxt, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL失败:不能找到流信息。"));
+ cleanup();
+ return avError;
+ }
+
+ if(!wavPath.isEmpty())
+ {
+ avError = avformat_open_input(&audioFmtCxt, wavPath.toUtf8(), 0, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV失败。"));
+ cleanup();
+ return avError;
+ }
+ avError = avformat_find_stream_info(audioFmtCxt, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV失败:不能找到流信息。"));
+ cleanup();
+ return avError;
+ }
+ }
+
+ // Get input video stream
+ videoStreamID = av_find_best_stream(videoFmtCxt, AVMEDIA_TYPE_VIDEO, -1, -1, &videoDecoder, 0);
+ if(videoStreamID == AVERROR_STREAM_NOT_FOUND)
+ {
+ emit showError(tr("打开MXL失败:不能找到流信息。"));
+ cleanup();
+ return -1;
+ }
+
+ if(videoFmtCxt->streams[videoStreamID]->codecpar->width != 3840 || videoFmtCxt->streams[videoStreamID]->codecpar->height !=2160)
+ {
+ emit showError(tr("打开MXL失败:不正确的视频尺寸。"));
+ cleanup();
+ return -1;
+ }
+
+ if(!wavPath.isEmpty())
+ {
+ audioStreamID = av_find_best_stream(audioFmtCxt, AVMEDIA_TYPE_AUDIO, -1, -1, &audioDecoder, 0);
+ if(audioStreamID == AVERROR_STREAM_NOT_FOUND)
+ {
+ emit showError(tr("打开WAV失败:不能找到流信息。"));
+ cleanup();
+ return -1;
+ }
+ }
+
+ // Open decoder
+ videoDecoderCxt = avcodec_alloc_context3(videoDecoder);
+ avError = avcodec_parameters_to_context(videoDecoderCxt, videoFmtCxt->streams[videoStreamID]->codecpar);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL失败:不能找到解码器。"));
+ cleanup();
+ return avError;
+ }
+ avError = avcodec_open2(videoDecoderCxt, videoDecoder, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开MXL失败:不能打开解码器。"));
+ cleanup();
+ return avError;
+ }
+
+ if(!wavPath.isEmpty())
+ {
+ audioDecoderCxt = avcodec_alloc_context3(audioDecoder);
+ avError = avcodec_parameters_to_context(audioDecoderCxt, audioFmtCxt->streams[audioStreamID]->codecpar);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV失败:不能找到解码器。"));
+ cleanup();
+ return avError;
+ }
+ avError = avcodec_open2(audioDecoderCxt, audioDecoder, 0);
+ if(avError < 0)
+ {
+ emit showError(tr("打开WAV失败:不能打开解码器。"));
+ cleanup();
+ return avError;
+ }
+ if(audioDecoderCxt->ch_layout.nb_channels > 2)
+ {
+ emit showError(tr("由于软件限制,MXLPlayer暂不能回放非单声道/立体声配置的WAV音频。\n这并不会影响您的WAV文件正常在杜比影院设备上的播放。\n您可以使用其它音频播放器检视该WAV音频文件。"));
+ cleanup();
+ return avError;
+ }
+ }
+
+ // Init filter
+ videoFilterGraph = avfilter_graph_alloc();
+
+ videoFilterSrc = avfilter_get_by_name("buffer");
+ char videoFilterSrcArgs[512];
+ snprintf(videoFilterSrcArgs, sizeof(videoFilterSrcArgs), "video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d", videoDecoderCxt->width, videoDecoderCxt->height, AV_PIX_FMT_YUV420P, videoFmtCxt->streams[videoStreamID]->time_base.num, videoFmtCxt->streams[videoStreamID]->time_base.den, videoDecoderCxt->sample_aspect_ratio.num, videoDecoderCxt->sample_aspect_ratio.den);
+ avError = avfilter_graph_create_filter(&videoFilterSrcCxt, videoFilterSrc, "in", videoFilterSrcArgs, 0, videoFilterGraph);
+
+ videoFilterSink = avfilter_get_by_name("buffersink");
+ avError = avfilter_graph_create_filter(&videoFilterSinkCxt, videoFilterSink, "out", 0, 0, videoFilterGraph);
+
+ videoFilterInput = avfilter_inout_alloc();
+ videoFilterInput -> name = av_strdup("in");
+ videoFilterInput -> filter_ctx = videoFilterSrcCxt;
+ videoFilterInput -> pad_idx = 0;
+ videoFilterInput -> next = NULL;
+
+ videoFilterOutput = avfilter_inout_alloc();
+ videoFilterOutput -> name = av_strdup("out");
+ videoFilterOutput -> filter_ctx = videoFilterSinkCxt;
+ videoFilterOutput -> pad_idx = 0;
+ videoFilterOutput -> next = NULL;
+
+ switch(size)
+ {
+ case AVP::kAVPLargeSize:
+ avError = avfilter_graph_parse_ptr(videoFilterGraph, filterGraphLarge, &videoFilterOutput, &videoFilterInput, 0);
+ break;
+ case AVP::kAVPMediumSize:
+ avError = avfilter_graph_parse_ptr(videoFilterGraph, filterGraphMedium, &videoFilterOutput, &videoFilterInput, 0);
+ break;
+ case AVP::kAVPSmallSize:
+ avError = avfilter_graph_parse_ptr(videoFilterGraph, filterGraphSmall, &videoFilterOutput, &videoFilterInput, 0);
+ break;
+ }
+
+ avError = avfilter_graph_config(videoFilterGraph, 0);
+
+ // Init scaler
+ scalerCxt = sws_getContext(videoDecoderCxt->width, videoDecoderCxt->height, videoDecoderCxt->pix_fmt, videoDecoderCxt->width, videoDecoderCxt->height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR, 0, 0, 0);
+
+ // Init resampler
+ if(!wavPath.isEmpty())
+ {
+ avError = swr_alloc_set_opts2(&resamplerCxt, &audioDecoderCxt->ch_layout, AV_SAMPLE_FMT_S16, 44100, &audioDecoderCxt->ch_layout, audioDecoderCxt->sample_fmt, audioDecoderCxt->sample_rate, 0, 0);
+ avError = swr_init(resamplerCxt);
+ }
+
+ // Allocate memory
+ vPacket = av_packet_alloc();
+ aPacket = av_packet_alloc();
+ frameIn = av_frame_alloc();
+ frameScaled = av_frame_alloc();
+ frameFiltered = av_frame_alloc();
+ frame = av_frame_alloc();
+
+ if(!wavPath.isEmpty())
+ {
+ iAudioBuffer = (uint8_t*)av_malloc(MAX_AUDIO_FRAME_SIZE * 2);
+ oAudioBuffer = (uint8_t*)av_malloc(MAX_AUDIO_FRAME_SIZE * 2);
+ audioQueue = av_audio_fifo_alloc(AV_SAMPLE_FMT_S16, audioDecoderCxt->ch_layout.nb_channels, 4096);
+ }
+
+ // Init SDL
+ SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER);
+ SDL_DisplayMode display;
+ SDL_GetDesktopDisplayMode(0, &display);
+ window = SDL_CreateWindow("AVPStudio - MXLPlayer", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, display.w, (int)((double)AVPHeight * ((double)display.w / (double)AVPWidth)), 0);
+ renderer = SDL_CreateRenderer(window, -1, 0);
+ texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, AVPWidth, AVPHeight);
+
+ if(!wavPath.isEmpty())
+ {
+ wantedSpec.freq = 44100;
+ wantedSpec.format = AUDIO_S16;
+ wantedSpec.channels = audioDecoderCxt->ch_layout.nb_channels;
+ wantedSpec.silence = 0;
+ wantedSpec.samples = 1024;
+ wantedSpec.callback = SDLFillAudio;
+ wantedSpec.userdata = audioDecoderCxt;
+
+ if(SDL_OpenAudio(&wantedSpec, 0) < 0)
+ {
+ emit showError(tr("无法打开音频设备。"));
+ cleanup();
+ return -1;
+ }
+ SDL_PauseAudio(1);
+ }
+
+ // Set info
+ emit setPositionBarMax(videoFmtCxt->streams[videoStreamID]->duration, av_q2d(videoFmtCxt->streams[videoStreamID]->time_base));
+
+ return 0;
+}
+
+void TPlayVideo::cleanup()
+{
+ SDL_CloseAudio();
+ SDL_DestroyTexture(texture);
+ SDL_DestroyRenderer(renderer);
+ SDL_DestroyWindow(window);
+
+ avformat_close_input(&videoFmtCxt);
+ avformat_free_context(videoFmtCxt);
+ avformat_close_input(&audioFmtCxt);
+ avformat_free_context(audioFmtCxt);
+
+ avcodec_free_context(&videoDecoderCxt);
+ avcodec_free_context(&audioDecoderCxt);
+
+ avfilter_free(videoFilterSrcCxt);
+ avfilter_free(videoFilterSinkCxt);
+
+ avfilter_graph_free(&videoFilterGraph);
+ avfilter_inout_free(&videoFilterInput);
+ avfilter_inout_free(&videoFilterOutput);
+
+ sws_freeContext(scalerCxt);
+ swr_free(&resamplerCxt);
+
+ av_packet_free(&vPacket);
+ av_packet_free(&aPacket);
+ av_frame_free(&frameIn);
+ av_frame_free(&frameFiltered);
+ av_frame_free(&frameScaled);
+ av_frame_free(&frame);
+
+ av_free(iAudioBuffer);
+ av_free(oAudioBuffer);
+
+ av_audio_fifo_free(audioQueue);
+ SDL_DestroyMutex(audioQueueMutex);
+}
+
+void TPlayVideo::notifyQuit()
+{
+ SDL_Event quitEvent;
+ quitEvent.type = SDL_CUSTOM_QUIT_EVENT;
+ SDL_PushEvent(&quitEvent);
+}
+
+void TPlayVideo::do_updatePosition(int val)
+{
+ newPosition = val;
+ SDL_Event positionUpdateEvent;
+ positionUpdateEvent.type = SDL_CUSTOM_POSITION_UPDATE_EVENT;
+ SDL_PushEvent(&positionUpdateEvent);
+}
+
+void TPlayVideo::do_play()
+{
+ refresherFlag = 0;
+ if(!wavPath.isEmpty())
+ SDL_PauseAudio(0);
+}
+
+void TPlayVideo::do_pause()
+{
+ refresherFlag = 1;
+ if(!wavPath.isEmpty())
+ SDL_PauseAudio(1);
+}
+
+void TPlayVideo::run()
+{
+ static int avError = 0;
+ int frameDuration = (int) 1000 / av_q2d(videoDecoderCxt->framerate);
+ threadRefresh = SDL_CreateThread(SDLRefresher, 0, &frameDuration);
+ if(!wavPath.isEmpty())
+ threadDecodeAudio = SDL_CreateThread(SDLAudioDecoder, 0, 0);
+
+ while(true)
+ {
+ while(SDL_PollEvent(&eventSDL))
+ {
+ if(eventSDL.type == SDL_CUSTOM_REFRESH_EVENT)
+ {
+ if(av_read_frame(videoFmtCxt, vPacket) == 0)
+ {
+ if(vPacket->stream_index == videoStreamID)
+ {
+ avError = avcodec_send_packet(videoDecoderCxt, vPacket);
+ while(true)
+ {
+ avError = avcodec_receive_frame(videoDecoderCxt, frameIn);
+ if(avError == AVERROR(EAGAIN) || avError == AVERROR_EOF)
+ break;
+
+ emit setPosition(frameIn->pkt_dts);
+
+ sws_scale_frame(scalerCxt, frameScaled, frameIn);
+
+ avError = av_buffersrc_add_frame(videoFilterSrcCxt, frameScaled);
+ avError = av_buffersink_get_frame(videoFilterSinkCxt, frameFiltered);
+
+ SDL_UpdateYUVTexture(texture, 0, frameFiltered->data[0], frameFiltered->linesize[0], frameFiltered->data[1], frameFiltered->linesize[1], frameFiltered->data[2], frameFiltered->linesize[2]);
+ SDL_RenderClear(renderer);
+ SDL_RenderCopy(renderer, texture, 0, 0);
+ SDL_RenderPresent(renderer);
+
+ av_frame_unref(frameIn);
+ av_frame_unref(frameScaled);
+ av_frame_unref(frameFiltered);
+ }
+ av_packet_unref(vPacket);
+ }
+ }
+ else
+ {
+ av_seek_frame(videoFmtCxt, videoStreamID, 0, AVSEEK_FLAG_BACKWARD);
+ if(!wavPath.isEmpty())
+ {
+ av_seek_frame(audioFmtCxt, audioStreamID, 0, AVSEEK_FLAG_BACKWARD);
+ SDL_PauseAudio(0);
+ }
+ }
+ }
+ else if(eventSDL.type == SDL_CUSTOM_POSITION_UPDATE_EVENT)
+ {
+ av_seek_frame(videoFmtCxt, videoStreamID, newPosition, AVSEEK_FLAG_ANY);
+ if(!wavPath.isEmpty())
+ {
+ av_seek_frame(audioFmtCxt, audioStreamID, av_rescale_q(newPosition, videoFmtCxt->streams[videoStreamID]->time_base, audioFmtCxt->streams[audioStreamID]->time_base), AVSEEK_FLAG_ANY);
+ SDL_LockMutex(audioQueueMutex);
+ av_audio_fifo_reset(audioQueue);
+ SDL_UnlockMutex(audioQueueMutex);
+ }
+ }
+ else if(eventSDL.type == SDL_CUSTOM_QUIT_EVENT)
+ {
+ refresherFlag = 2;
+ cleanup();
+ return;
+ }
+ else if(eventSDL.type == SDL_QUIT)
+ {
+ emit sdlQuit();
+ }
+ }
+ }
+}
diff --git a/tools/mxlplayer/src/playvideo.h b/tools/mxlplayer/src/playvideo.h
new file mode 100644
index 0000000..2053ac3
--- /dev/null
+++ b/tools/mxlplayer/src/playvideo.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 Steven Song (izwb003)
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#ifndef TPLAYVIDEO_H
+#define TPLAYVIDEO_H
+
+#define SDL_MAIN_HANDLED
+
+#include "avpsettings.h"
+
+#include
+
+class TPlayVideo : public QThread
+{
+ Q_OBJECT
+public:
+ explicit TPlayVideo(QObject *parent = nullptr, QString mxlPath = "", QString wavPath = "", AVP::AVPSize size = AVP::kAVPMediumSize);
+
+ int init();
+
+ void cleanup();
+
+ void notifyQuit();
+
+signals:
+ void showError(QString errorMsg);
+
+ void setPositionBarMax(int val, double timebase);
+
+ void setPosition(int val);
+
+ void sdlQuit();
+
+private:
+ QString mxlPath = "";
+ QString wavPath = "";
+ AVP::AVPSize size = AVP::kAVPMediumSize;
+ int AVPWidth = 4632;
+ int AVPHeight = 1080;
+
+ int newPosition = 0;
+
+private slots:
+ void do_updatePosition(int val);
+
+ void do_play();
+
+ void do_pause();
+
+protected:
+ void run();
+};
+
+#endif // TPLAYVIDEO_H
diff --git a/tools/mxlplayer/ts/mxlplayer_zh_CN.ts b/tools/mxlplayer/ts/mxlplayer_zh_CN.ts
new file mode 100644
index 0000000..630fd35
--- /dev/null
+++ b/tools/mxlplayer/ts/mxlplayer_zh_CN.ts
@@ -0,0 +1,3 @@
+
+
+
diff --git a/tools/wavgenerator/src/genprocess.cpp b/tools/wavgenerator/src/genprocess.cpp
index 9f64eca..1c0fdbd 100644
--- a/tools/wavgenerator/src/genprocess.cpp
+++ b/tools/wavgenerator/src/genprocess.cpp
@@ -69,6 +69,8 @@ void TGenProcess::run()
SwrContext *resamplerCxt = NULL;
+ uint64_t audioPTSCounter = 0;
+
AVStream *oAudioStream = NULL;
AVFormatContext *oAudioFmtCxt = NULL;
const AVCodec *oAudioEncoder = NULL;
@@ -215,11 +217,22 @@ void TGenProcess::run()
avError = swr_config_frame(resamplerCxt, frameOutput, frameFiltered);
avError = swr_convert_frame(resamplerCxt, frameOutput, frameFiltered);
+ // Make timestamp
+ frameOutput -> pts = audioPTSCounter;
+ audioPTSCounter += oAudioEncoderCxt->frame_size;
+
// Encode
avError = avcodec_send_frame(oAudioEncoderCxt, frameOutput);
avError = avcodec_receive_packet(oAudioEncoderCxt, packet);
avError = av_write_frame(oAudioFmtCxt, packet);
+
+ // Unref frames
+ av_frame_unref(frameInput);
+ av_frame_unref(frameFiltered);
+ av_frame_unref(frameOutput);
}
+ // Unref packet
+ av_packet_unref(packet);
}
}
@@ -252,7 +265,14 @@ void TGenProcess::run()
av_frame_free(&frameFiltered);
av_frame_free(&frameOutput);
+ avfilter_free(volumeFilterCxt);
+ avfilter_free(volumeFilterSrcCxt);
+ avfilter_free(volumeFilterSinkCxt);
+
avfilter_graph_free(&volumeFilterGraph);
swr_free(&resamplerCxt);
+
+ if(avError == 0)
+ emit completed();
}
diff --git a/tools/wavgenerator/src/genprocess.h b/tools/wavgenerator/src/genprocess.h
index 1571cd3..d3f82d8 100644
--- a/tools/wavgenerator/src/genprocess.h
+++ b/tools/wavgenerator/src/genprocess.h
@@ -37,6 +37,7 @@ class TGenProcess : public QThread
void setProgressMax(int64_t num);
void setProgress(int64_t num);
void showError(QString errorStr, QString title);
+ void completed();
};
#endif // TGENPROCESS_H
diff --git a/tools/wavgenerator/src/mainwindow.cpp b/tools/wavgenerator/src/mainwindow.cpp
index 8df41af..302cc21 100644
--- a/tools/wavgenerator/src/mainwindow.cpp
+++ b/tools/wavgenerator/src/mainwindow.cpp
@@ -18,8 +18,6 @@
#include "mainwindow.h"
#include "./ui_mainwindow.h"
-#include "genprocess.h"
-
#include
#include
#include
@@ -62,6 +60,7 @@ void MainWindow::do_showError(QString errorStr, QString title)
void MainWindow::do_processFinished()
{
+ ui->progressBar->setValue(ui->progressBar->maximum());
ui->statusbar->showMessage(tr("完成"));
this->setWindowTitle("AVPStudio WAVGenerator");
ui->pushButtonConvert->setEnabled(true);
@@ -100,11 +99,11 @@ void MainWindow::on_pushButtonConvert_clicked()
return;
}
- TGenProcess *genProcess = new TGenProcess(this, ui->lineEditInputFile->text(), ui->lineEditOutputFile->text(), ui->spinBoxVolume->value());
+ genProcess = new TGenProcess(this, ui->lineEditInputFile->text(), ui->lineEditOutputFile->text(), ui->spinBoxVolume->value());
connect(genProcess, SIGNAL(setProgressMax(int64_t)), this, SLOT(do_setProgressMax(int64_t)));
connect(genProcess, SIGNAL(setProgress(int64_t)), this, SLOT(do_setProgress(int64_t)));
- connect(genProcess, SIGNAL(finished()), this, SLOT(do_processFinished()));
+ connect(genProcess, SIGNAL(completed()), this, SLOT(do_processFinished()));
connect(genProcess, SIGNAL(showError(QString,QString)), this, SLOT(do_showError(QString,QString)));
connect(genProcess, &QThread::finished, genProcess, &QObject::deleteLater);
@@ -134,3 +133,14 @@ void MainWindow::on_checkBoxDolbyNaming_stateChanged(int arg1)
}
}
+
+void MainWindow::on_pushButtonCancel_clicked()
+{
+ genProcess->terminate();
+ ui->statusbar->showMessage(tr("已取消。"));
+ this->setWindowTitle("AVPStudio WAVGenerator");
+ ui->pushButtonConvert->setEnabled(true);
+ ui->pushButtonCancel->setEnabled(false);
+ ui->progressBar->setValue(0);
+}
+
diff --git a/tools/wavgenerator/src/mainwindow.h b/tools/wavgenerator/src/mainwindow.h
index 8f9a480..bbe8577 100644
--- a/tools/wavgenerator/src/mainwindow.h
+++ b/tools/wavgenerator/src/mainwindow.h
@@ -18,6 +18,8 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
+#include "genprocess.h"
+
#include
#include
@@ -52,7 +54,11 @@ private slots:
void on_checkBoxDolbyNaming_stateChanged(int arg1);
+ void on_pushButtonCancel_clicked();
+
private:
Ui::MainWindow *ui;
+
+ TGenProcess *genProcess;
};
#endif // MAINWINDOW_H