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) + + 工具(&T) - + + @@ -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... + + + + + + + + + + + 0 + 0 + 491 + 21 + + + + + + + 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