From af74a954f8dd17a26ac5f3cfa2f3ae34471890df Mon Sep 17 00:00:00 2001 From: Ryan <865833921@qq.com> Date: Fri, 27 Jun 2025 09:23:46 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppScope/app.json5 | 10 + AppScope/resources/base/element/string.json | 8 + AppScope/resources/base/media/app_icon.png | Bin 0 -> 2777 bytes LICENSE | 78 +++ README.en.md | 36 -- README.md | 155 ++++- build-profile.json5 | 56 ++ entry/build-profile.json5 | 31 + entry/hvigorfile.ts | 6 + entry/oh-package.json5 | 26 + entry/src/main/cpp/CMakeLists.txt | 32 + .../main/cpp/capbilities/AudioCapturer.cpp | 76 +++ .../src/main/cpp/capbilities/AudioDecoder.cpp | 129 ++++ .../src/main/cpp/capbilities/AudioEncoder.cpp | 147 +++++ entry/src/main/cpp/capbilities/Demuxer.cpp | 126 ++++ entry/src/main/cpp/capbilities/Muxer.cpp | 104 +++ .../src/main/cpp/capbilities/VideoDecoder.cpp | 139 ++++ .../src/main/cpp/capbilities/VideoEncoder.cpp | 171 +++++ .../cpp/capbilities/include/AudioCapturer.h | 40 ++ .../cpp/capbilities/include/AudioDecoder.h | 43 ++ .../cpp/capbilities/include/AudioEncoder.h | 48 ++ .../main/cpp/capbilities/include/Demuxer.h | 45 ++ .../src/main/cpp/capbilities/include/Muxer.h | 44 ++ .../cpp/capbilities/include/VideoDecoder.h | 45 ++ .../cpp/capbilities/include/VideoEncoder.h | 49 ++ entry/src/main/cpp/common/SampleCallback.cpp | 126 ++++ entry/src/main/cpp/common/SampleCallback.h | 38 ++ entry/src/main/cpp/common/SampleInfo.h | 162 +++++ .../cpp/common/dfx/error/AVCodecSampleError.h | 24 + .../cpp/common/dfx/log/AVCodecSampleLog.h | 83 +++ entry/src/main/cpp/sample/player/Player.cpp | 592 ++++++++++++++++++ entry/src/main/cpp/sample/player/Player.h | 109 ++++ .../main/cpp/sample/player/PlayerNative.cpp | 111 ++++ .../src/main/cpp/sample/player/PlayerNative.h | 33 + entry/src/main/cpp/types/libplayer/index.d.ts | 29 + .../main/cpp/types/libplayer/oh-package.json5 | 21 + entry/src/main/ets/common/CommonConstants.ets | 167 +++++ entry/src/main/ets/common/utils/Logger.ets | 43 ++ entry/src/main/ets/common/utils/TimeUtils.ets | 26 + .../main/ets/entryability/EntryAbility.ets | 60 ++ .../entrybackupability/EntryBackupAbility.ets | 27 + entry/src/main/ets/model/VideoDataModel.ets | 40 ++ entry/src/main/ets/pages/Index.ets | 110 ++++ entry/src/main/ets/pages/VideoPlayer.ets | 239 +++++++ entry/src/main/ets/pages/VideoTranscoding.ets | 251 ++++++++ entry/src/main/module.json5 | 52 ++ .../main/resources/base/element/color.json | 12 + .../main/resources/base/element/float.json | 72 +++ .../main/resources/base/element/string.json | 64 ++ .../base/media/camera_pause_video_4x.png | Bin 0 -> 9196 bytes .../main/resources/base/media/gearshape.svg | 2 + entry/src/main/resources/base/media/icon.png | Bin 0 -> 3639 bytes entry/src/main/resources/base/media/pause.svg | 1 + entry/src/main/resources/base/media/play.svg | 1 + .../base/media/transcoding_success.svg | 2 + .../resources/base/profile/backup_config.json | 3 + .../resources/base/profile/main_pages.json | 5 + .../main/resources/en_US/element/string.json | 64 ++ .../main/resources/zh_CN/element/string.json | 65 ++ hvigor/hvigor-config.json5 | 22 + hvigorfile.ts | 6 + oh-package.json5 | 22 + screenshots/device/AVCodec_Index.png | Bin 0 -> 40122 bytes 63 files changed, 4267 insertions(+), 61 deletions(-) create mode 100644 AppScope/app.json5 create mode 100644 AppScope/resources/base/element/string.json create mode 100644 AppScope/resources/base/media/app_icon.png create mode 100644 LICENSE delete mode 100644 README.en.md create mode 100644 build-profile.json5 create mode 100644 entry/build-profile.json5 create mode 100644 entry/hvigorfile.ts create mode 100644 entry/oh-package.json5 create mode 100644 entry/src/main/cpp/CMakeLists.txt create mode 100644 entry/src/main/cpp/capbilities/AudioCapturer.cpp create mode 100644 entry/src/main/cpp/capbilities/AudioDecoder.cpp create mode 100644 entry/src/main/cpp/capbilities/AudioEncoder.cpp create mode 100644 entry/src/main/cpp/capbilities/Demuxer.cpp create mode 100644 entry/src/main/cpp/capbilities/Muxer.cpp create mode 100644 entry/src/main/cpp/capbilities/VideoDecoder.cpp create mode 100644 entry/src/main/cpp/capbilities/VideoEncoder.cpp create mode 100644 entry/src/main/cpp/capbilities/include/AudioCapturer.h create mode 100644 entry/src/main/cpp/capbilities/include/AudioDecoder.h create mode 100644 entry/src/main/cpp/capbilities/include/AudioEncoder.h create mode 100644 entry/src/main/cpp/capbilities/include/Demuxer.h create mode 100644 entry/src/main/cpp/capbilities/include/Muxer.h create mode 100644 entry/src/main/cpp/capbilities/include/VideoDecoder.h create mode 100644 entry/src/main/cpp/capbilities/include/VideoEncoder.h create mode 100644 entry/src/main/cpp/common/SampleCallback.cpp create mode 100644 entry/src/main/cpp/common/SampleCallback.h create mode 100644 entry/src/main/cpp/common/SampleInfo.h create mode 100644 entry/src/main/cpp/common/dfx/error/AVCodecSampleError.h create mode 100644 entry/src/main/cpp/common/dfx/log/AVCodecSampleLog.h create mode 100644 entry/src/main/cpp/sample/player/Player.cpp create mode 100644 entry/src/main/cpp/sample/player/Player.h create mode 100644 entry/src/main/cpp/sample/player/PlayerNative.cpp create mode 100644 entry/src/main/cpp/sample/player/PlayerNative.h create mode 100644 entry/src/main/cpp/types/libplayer/index.d.ts create mode 100644 entry/src/main/cpp/types/libplayer/oh-package.json5 create mode 100644 entry/src/main/ets/common/CommonConstants.ets create mode 100644 entry/src/main/ets/common/utils/Logger.ets create mode 100644 entry/src/main/ets/common/utils/TimeUtils.ets create mode 100644 entry/src/main/ets/entryability/EntryAbility.ets create mode 100644 entry/src/main/ets/entrybackupability/EntryBackupAbility.ets create mode 100644 entry/src/main/ets/model/VideoDataModel.ets create mode 100644 entry/src/main/ets/pages/Index.ets create mode 100644 entry/src/main/ets/pages/VideoPlayer.ets create mode 100644 entry/src/main/ets/pages/VideoTranscoding.ets create mode 100644 entry/src/main/module.json5 create mode 100644 entry/src/main/resources/base/element/color.json create mode 100644 entry/src/main/resources/base/element/float.json create mode 100644 entry/src/main/resources/base/element/string.json create mode 100644 entry/src/main/resources/base/media/camera_pause_video_4x.png create mode 100644 entry/src/main/resources/base/media/gearshape.svg create mode 100644 entry/src/main/resources/base/media/icon.png create mode 100644 entry/src/main/resources/base/media/pause.svg create mode 100644 entry/src/main/resources/base/media/play.svg create mode 100644 entry/src/main/resources/base/media/transcoding_success.svg create mode 100644 entry/src/main/resources/base/profile/backup_config.json create mode 100644 entry/src/main/resources/base/profile/main_pages.json create mode 100644 entry/src/main/resources/en_US/element/string.json create mode 100644 entry/src/main/resources/zh_CN/element/string.json create mode 100644 hvigor/hvigor-config.json5 create mode 100644 hvigorfile.ts create mode 100644 oh-package.json5 create mode 100644 screenshots/device/AVCodec_Index.png diff --git a/AppScope/app.json5 b/AppScope/app.json5 new file mode 100644 index 0000000..936a19d --- /dev/null +++ b/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.samples.AVCodecBufferMode", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/AppScope/resources/base/element/string.json b/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..3ad20b7 --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "AVCodecBufferMode" + } + ] +} diff --git a/AppScope/resources/base/media/app_icon.png b/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a39445dc87828b76fed6d2ec470dd455c45319e3 GIT binary patch literal 2777 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 literal 0 HcmV?d00001 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..18795a4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,78 @@ + Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Apache License, Version 2.0 +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +1.You must give any other recipients of the Work or Derivative Works a copy of this License; and +2.You must cause any modified files to carry prominent notices stating that You changed the files; and +3.You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +4.If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 4216cbd..0000000 --- a/README.en.md +++ /dev/null @@ -1,36 +0,0 @@ -# AVCodecBufferMode - -#### Description -基于buffer模式进行视频转码 - -#### Software Architecture -Software architecture description - -#### Installation - -1. xxxx -2. xxxx -3. xxxx - -#### Instructions - -1. xxxx -2. xxxx -3. xxxx - -#### Contribution - -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request - - -#### Gitee Feature - -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/README.md b/README.md index d0dcda5..ddbc99e 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,142 @@ -# AVCodecBufferMode +# 基于Buffer模式进行视频转码 -#### 介绍 -基于buffer模式进行视频转码 +### 介绍 +本实例基于AVCodec能力,实现了基于Buffer模式的视频转码功能。通过调用Native侧的编码器,解码器,以及封装和解封装功能,完成从视频解封装、解码、编码、封装的过程。基于本实例可以帮助开发者理解Buffer模式,并通过Buffer模式进行转码。 -#### 软件架构 -软件架构说明 +### 播放支持的原子能力规格 +| 媒体格式 | 封装格式 | 码流格式 | +|------|:--------|:------------------------------------| +| 视频 | mp4 | 视频码流:H.264/H.265, 音频码流:AudioVivid | +| 视频 | mkv | 视频码流:H.264/H.265, 音频码流:aac/mp3/opus | +| 视频 | mpeg-ts | 视频码流:H.264, 音频码流:AudioVivid | +### 录制支持的原子能力规格 -#### 安装教程 +| 封装格式 | 视频编解码类型 | +|------|-------------| +| mp4 | H.264/H.265 | -1. xxxx -2. xxxx -3. xxxx -#### 使用说明 +### 效果预览 +| 应用主界面 | +|------------------------------------------------------------| +| ![AVCodec_Index.png](screenshots/device/AVCodec_Index.png) | -1. xxxx -2. xxxx -3. xxxx +### 使用说明 +1. 进入首页后,点击选择文件选择需要转码的视频。 +2. 给需要转码的视频配置对应的参数。 +3. 点击开始转码后即可开始转码。 +4. 在转码完成后,会跳转到下一个页面,可以查看转码完成的视频。 -#### 参与贡献 -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +### 工程目录 -#### 特技 +``` +├──entry/src/main/cpp // Native层 +│ ├──capbilities // 能力接口和实现 +│ │ ├──include // 能力接口 +│ │ ├──AudioDecoder.cpp // 音频解码实现 +│ │ ├──AudioEncoder.cpp // 音频解码实现 +│ │ ├──Demuxer.cpp // 解封装实现 +│ │ ├──Muxer.cpp // 封装实现 +│ │ ├──VideoDecoder.cpp // 视频解码实现 +│ │ └──VideoEncoder.cpp // 视频编码实现 +│ ├──common // 公共模块 +│ │ ├──dfx // 日志 +│ │ ├──SampleCallback.cpp // 编解码回调实现 +│ │ ├──SampleCallback.h // 编解码回调定义 +│ │ └──SampleInfo.h // 功能实现公共类 +│ ├──sample // Native层 +│ │ └──player // Native层转码接口和实现 +│ │ ├──Player.cpp // Native层转码功能调用逻辑的实现 +│ │ ├──Player.h // Native层转码功能调用逻辑的接口 +│ │ ├──PlayerNative.cpp // Native层转码的入口 +│ │ └──PlayerNative.h +│ ├──types // Native层暴露上来的接口 +│ │ └──libplayer // 转码模块暴露给UI层的接口 +│ └──CMakeLists.txt // 编译入口 +├──ets // UI层 +│ ├──common // 公共模块 +│ │ ├──utils // 共用的工具类 +│ │ │ ├──TimeUtils.ets // 获取当前时间 +│ │ │ └──Logger.ets // 日志工具 +│ │ └──CommonConstants.ets // 参数常量 +│ ├──entryability // 应用的入口 +│ │ └──EntryAbility.ets +│ ├──entrybackupability +│ │ └──EntryBackupAbility.ets +│ ├──model +│ │ └──CameraDataModel.ets // 相机参数数据类 +│ └──pages // EntryAbility 包含的页面 +│ ├──Index.ets // 首页/视频选择页面 +│ ├──VideoPlayer.ets // 视频播放页面 +│ └──VideoTranscoding.ets // 视频转码页面 +├──resources // 用于存放应用所用到的资源文件 +│ ├──base // 该目录下的资源文件会被赋予唯一的ID +│ │ ├──element // 用于存放字体和颜色 +│ │ ├──media // 用于存放图片 +│ │ └──profile // 应用入口首页 +│ ├──en_US // 设备语言是美式英文时,优先匹配此目录下资源 +│ └──zh_CN // 设备语言是简体中文时,优先匹配此目录下资源 +└──module.json5 // 模块配置信息 +``` -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +### 具体实现 + +#### *录制* +##### UI层 +1. 在UI层Index页面,用户点击“录制”后,会拉起半模态界面,用户确认保存录制文件到图库。录制结束后,文件会存放于图库。 +2. 选择好文件后,会用刚刚打开的fd,和用户预设的录制参数,掉起ArkTS的initNative,待初始化结束后,调用OH_NativeWindow_GetSurfaceId接口,得到NativeWindow的surfaceId,并把surfaceId回调回UI层。 +3. UI层拿到编码器给的surfaceId后,调起页面路由,携带该surfaceId,跳转到Recorder页面; +4. 录制页面XComponent构建时,会调起.onLoad()方法,此方法首先会拿到XComponent的surfaceId,然后调起createDualChannelPreview(),此函数会建立一个相机生产,XComponent和编码器的surface消费的生产消费模型。 + +##### Native层 +1. 进入录制界面后,编码器启动,开始对UI层相机预览流进行编码。 +2. 编码器每编码成功一帧,sample_callback.cpp的输出回调OnNewOutputBuffer()就会调起一次,此时用户会拿到AVCodec框架给出的OH_AVBuffer; +3. 在输出回调中,用户需手动把帧buffer、index存入输出队列中,并通知输出线程解锁; +4. 在输出线程中,把上一步的帧信息储存为bufferInfo后,pop出队; +5. 在输出线程中,使用上一步的bufferInfo,调用封装接口WriteSample后,这一帧被封装入MP4中; +6. 最后调用FreeOutputBuffer接口后,这一帧buffer释放回AVCodec框架,实现buffer轮转。 + +#### *播放* +##### UI层 +1. 在UI层Index页面,用户点击播放按钮后,触发点击事件,调起selectFile()函数,该函数会调起图库的选择文件模块,拿到用户选取文件的路径; +2. 用户选择文件成功后,调起play()函数,该函数会根据上一步获取到的路径,打开一个文件,并获取到该文件的大小,改变按钮状态为不可用,之后调起ArkTS层暴露给应用层的playNative()接口; +3. 根据playNative字段,调起PlayerNative::Play()函数,此处会注册播放结束的回调。 +4. 播放结束时,Callback()中napi_call_function()接口调起,通知应用层,恢复按钮状态为可用。 + +##### ArkTS层 +1. 在PlayerNative.cpp的Init()中调用PluginManager()中的Export()方法,注册OnSurfaceCreatedCB()回调,当屏幕上出现新的XComponent时,将其转换并赋给单例类PluginManager中的pluginWindow_; + +##### Native层 +1. 具体实现原理: + - 解码器Start后,解码器每拿到一帧,OnNeedInputBuffer就会被调起一次,AVCodec框架会给用户一个OH_AVBuffer。 + - 在输入回调中,用户需手动把帧buffer、index存入输入队列中,并同时输入线程解锁。 + - 在输入线程中,把上一步的帧信息储存为bufferInfo后,pop出队。 + - 在输入线程中,使用上一步的bufferInfo,调用ReadSample接口解封装帧数据。 + - 在输入线程中,使用解封装后的bufferInfo,调用解码的PushInputData接口,此时这片buffer用完,返回框架,实现buffer轮转。 + - PushInputData后,这一帧开始解码,每解码完成一帧,输出回调会被调起一次,用户需手动把帧buffer、index存入输出队列中。 + - 在输出线程中,把上一步的帧信息储存为bufferInfo后,pop出队。 + - 在输出线程中,调用FreeOutputData接口后,就会送显并释放buffer。释放的buffer会返回框架,实现buffer轮转。 +2. 解码器config阶段,OH_VideoDecoder_SetSurface接口的入参OHNativeWindow*,即为PluginManager中的pluginWindow_。 +3. 解码器config阶段,SetCallback接口,sample_callback.cpp的输入输出回调需将回调上来的帧buffer和index存入一个用户自定义容器sample_info.h中,方便后续操作。 +4. Player.cpp的Start()起两个专门用于输入和输出的线程。 + +### 相关权限 + +- 无 + +### 依赖 + +- 不涉及 + +### 约束与限制 + +1. 本示例仅支持标准系统上运行,支持设备:华为手机; + +2. HarmonyOS系统:HarmonyOS 5.0.0 Release及以上; + +3. DevEco Studio版本:DevEco Studio 5.0.0 Release及以上; + +4. HarmonyOS SDK版本:HarmonyOS 5.0.0 Release SDK及以上。 \ No newline at end of file diff --git a/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000..3f0c5d3 --- /dev/null +++ b/build-profile.json5 @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\xu\\.ohos\\config\\default_avcodec-buffer-mode_6-gFMlm-n7p76xoOjknDyNhxaNlAf4GDhMTN0eJLj_o=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001A28B2528533563CCD9BEFC82B94CADA661D1EAA4C06FF43B58A8801D23BC8C68AB3373AF03745", + "profile": "C:\\Users\\xu\\.ohos\\config\\default_avcodec-buffer-mode_6-gFMlm-n7p76xoOjknDyNhxaNlAf4GDhMTN0eJLj_o=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\xu\\.ohos\\config\\default_avcodec-buffer-mode_6-gFMlm-n7p76xoOjknDyNhxaNlAf4GDhMTN0eJLj_o=.p12", + "storePassword": "0000001AD15B83BB0B7C6620498420192CB14C189615D040BB5D63B7DD07EB7521E8362E633C79835E07" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "5.0.0(12)", + "runtimeOS": "HarmonyOS", + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 new file mode 100644 index 0000000..6e8afeb --- /dev/null +++ b/entry/build-profile.json5 @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "apiType": "stageMode", + "buildOption": { + "externalNativeOptions": { + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "", + "abiFilters": ["arm64-v8a", "x86_64"] + } + }, + "targets": [ + { + "name": "default" + } + ] +} \ No newline at end of file diff --git a/entry/hvigorfile.ts b/entry/hvigorfile.ts new file mode 100644 index 0000000..c6edcd9 --- /dev/null +++ b/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/entry/oh-package.json5 b/entry/oh-package.json5 new file mode 100644 index 0000000..fd87d3d --- /dev/null +++ b/entry/oh-package.json5 @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "libplayer.so": "file:./src/main/cpp/types/libplayer" + } +} \ No newline at end of file diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..d488abd --- /dev/null +++ b/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,32 @@ +# the minimum version of CMake. +cmake_minimum_required(VERSION 3.4.1) +project(videoCodecSample) + +set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}) + +include_directories(${NATIVERENDER_ROOT_PATH} + ${NATIVERENDER_ROOT_PATH}/capbilities/include + ${NATIVERENDER_ROOT_PATH}/common + ${NATIVERENDER_ROOT_PATH}/common/dfx/err + ${NATIVERENDER_ROOT_PATH}/common/dfx/log + ${NATIVERENDER_ROOT_PATH}/sample/player +) + +set(BASE_LIBRARY + libace_napi.z.so libEGL.so libGLESv3.so libace_ndk.z.so libuv.so libhilog_ndk.z.so + libnative_media_codecbase.so libnative_media_core.so libnative_media_vdec.so libnative_window.so + libnative_media_venc.so libnative_media_acodec.so libnative_media_avdemuxer.so libnative_media_avsource.so libnative_media_avmuxer.so + libohaudio.so +) +add_library(player SHARED sample/player/PlayerNative.cpp + sample/player/Player.cpp + capbilities/Demuxer.cpp + capbilities/VideoDecoder.cpp + capbilities/AudioDecoder.cpp + capbilities/Muxer.cpp + capbilities/VideoEncoder.cpp + capbilities/AudioEncoder.cpp + common/SampleCallback.cpp +) + +target_link_libraries(player PUBLIC ${BASE_LIBRARY}) \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/AudioCapturer.cpp b/entry/src/main/cpp/capbilities/AudioCapturer.cpp new file mode 100644 index 0000000..527c82d --- /dev/null +++ b/entry/src/main/cpp/capbilities/AudioCapturer.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AudioCapturer.h" +#include "SampleCallback.h" + + +AudioCapturer::~AudioCapturer() +{ + AudioCapturerRelease(); +} + +// AudioCapturer Callback +static int32_t AudioCapturerOnReadData(OH_AudioCapturer *capturer, void *userData, void *buffer, int32_t bufferLen) +{ + (void)capturer; + CodecUserData *codecUserData = static_cast(userData); + if (codecUserData != nullptr) { + std::unique_lock lock(codecUserData->inputMutex); + codecUserData->WriteCache(buffer, bufferLen); + codecUserData->inputCond.notify_all(); + } + return 0; +} + +void AudioCapturer::AudioCapturerInit(SampleInfo &sampleInfo, CodecUserData *audioEncContext) +{ + AudioCapturerRelease(); + + // Create builder + OH_AudioStream_Type type = AUDIOSTREAM_TYPE_CAPTURER; + OH_AudioStreamBuilder_Create(&builder_, type); + // set params and callbacks + OH_AudioStreamBuilder_SetSamplingRate(builder_, sampleInfo.audioSampleRate); + OH_AudioStreamBuilder_SetChannelCount(builder_, sampleInfo.audioChannelCount); + OH_AudioStreamBuilder_SetSampleFormat(builder_, AUDIOSTREAM_SAMPLE_S16LE); + OH_AudioStreamBuilder_SetLatencyMode(builder_, AUDIOSTREAM_LATENCY_MODE_NORMAL); + OH_AudioStreamBuilder_SetEncodingType(builder_, AUDIOSTREAM_ENCODING_TYPE_RAW); + OH_AudioCapturer_Callbacks callbacks; + callbacks.OH_AudioCapturer_OnReadData = AudioCapturerOnReadData; + OH_AudioStreamBuilder_SetCapturerCallback(builder_, callbacks, audioEncContext); + // create OH_AudioCapturer + OH_AudioStreamBuilder_GenerateCapturer(builder_, &audioCapturer_); +} + +void AudioCapturer::AudioCapturerStart() +{ + if (audioCapturer_ != nullptr) { + OH_AudioCapturer_Start(audioCapturer_); + } +} + +void AudioCapturer::AudioCapturerRelease() +{ + if (audioCapturer_ != nullptr) { + OH_AudioCapturer_Stop(audioCapturer_); + OH_AudioCapturer_Release(audioCapturer_); + audioCapturer_ = nullptr; + } + if (builder_ != nullptr) { + OH_AudioStreamBuilder_Destroy(builder_); + builder_ = nullptr; + } +} \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/AudioDecoder.cpp b/entry/src/main/cpp/capbilities/AudioDecoder.cpp new file mode 100644 index 0000000..b51bb78 --- /dev/null +++ b/entry/src/main/cpp/capbilities/AudioDecoder.cpp @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AudioDecoder.h" +#include "dfx/error/AVCodecSampleError.h" + +#undef LOG_TAG +#define LOG_TAG "AudioDecoder" + +AudioDecoder::~AudioDecoder() { Release(); } + +int32_t AudioDecoder::Create(const std::string &codecMime) { + decoder_ = OH_AudioCodec_CreateByMime(codecMime.c_str(), false); + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioDecoder::SetCallback(CodecUserData *codecUserData) { + int32_t ret = AV_ERR_OK; + ret = OH_AudioCodec_RegisterCallback(decoder_, + {SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, + SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer}, + codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret); + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioDecoder::Configure(const SampleInfo &sampleInfo) { + OH_AVFormat *format = OH_AVFormat_Create(); + CHECK_AND_RETURN_RET_LOG(format != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "AVFormat create failed"); + + OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUDIO_SAMPLE_FORMAT, SAMPLE_S16LE); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_CHANNEL_COUNT, sampleInfo.audioChannelCount); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_SAMPLE_RATE, sampleInfo.audioSampleRate); + OH_AVFormat_SetLongValue(format, OH_MD_KEY_CHANNEL_LAYOUT, sampleInfo.audioChannelLayout); + if (sampleInfo.codecConfigLen > 0) { + AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ====== codecConfig:%{public}p, len:%{public}i, " + "adts:${public}i, 0:0x%{public}02x, 1:0x%{public}02x", + sampleInfo.codecConfig, sampleInfo.codecConfigLen, sampleInfo.aacAdts, + sampleInfo.codecConfig[0], sampleInfo.codecConfig[1]); + uint8_t tmpCodecConfig[2]; + tmpCodecConfig[0] = 0x13; // 0x11 + tmpCodecConfig[1] = 0x10; // 0x90 + tmpCodecConfig[0] = sampleInfo.codecConfig[0]; // 0x11 + tmpCodecConfig[1] = sampleInfo.codecConfig[1]; // 0x90 + AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ====== 0:0x%{public}02x, 1:0x%{public}02x", tmpCodecConfig[0], + tmpCodecConfig[1]); + OH_AVFormat_SetBuffer(format, OH_MD_KEY_CODEC_CONFIG, sampleInfo.codecConfig, sampleInfo.codecConfigLen); + } + + AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ======"); + int ret = OH_AudioCodec_Configure(decoder_, format); + AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ======"); + OH_AVFormat_Destroy(format); + format = nullptr; + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Config failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioDecoder::Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData) { + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + CHECK_AND_RETURN_RET_LOG(codecUserData != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Invalid param: codecUserData"); + + // Configure audio decoder + int32_t ret = Configure(sampleInfo); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Configure failed"); + + // SetCallback for audio decoder + ret = SetCallback(codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, + "Set callback failed, ret: %{public}d", ret); + + // Prepare audio decoder + { + int ret = OH_AudioCodec_Prepare(decoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Prepare failed, ret: %{public}d", ret); + } + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioDecoder::Start() { + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + + int ret = OH_AudioCodec_Start(decoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioDecoder::PushInputBuffer(CodecBufferInfo &info) { + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + int32_t ret = OH_AVBuffer_SetBufferAttr(reinterpret_cast(info.buffer), &info.attr); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set avbuffer attr failed"); + ret = OH_AudioCodec_PushInputBuffer(decoder_, info.bufferIndex); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Push input data failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioDecoder::FreeOutputBuffer(uint32_t bufferIndex, bool render) { + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + + int32_t ret = AVCODEC_SAMPLE_ERR_OK; + ret = OH_AudioCodec_FreeOutputBuffer(decoder_, bufferIndex); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Free output data failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioDecoder::Release() { + if (decoder_ != nullptr) { + OH_AudioCodec_Flush(decoder_); + OH_AudioCodec_Stop(decoder_); + OH_AudioCodec_Destroy(decoder_); + decoder_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/AudioEncoder.cpp b/entry/src/main/cpp/capbilities/AudioEncoder.cpp new file mode 100644 index 0000000..d6bf44b --- /dev/null +++ b/entry/src/main/cpp/capbilities/AudioEncoder.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AudioEncoder.h" + +#undef LOG_TAG +#define LOG_TAG "AudioEncoder" + +namespace { +constexpr int LIMIT_LOGD_FREQUENCY = 50; +} // namespace + +AudioEncoder::~AudioEncoder() +{ + Release(); +} + +int32_t AudioEncoder::Create(const std::string &codecMime) +{ + encoder_ = OH_AudioCodec_CreateByMime(codecMime.c_str(), true); + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::SetCallback(CodecUserData *codecUserData) +{ + int32_t ret = AV_ERR_OK; + ret = OH_AudioCodec_RegisterCallback(encoder_, + { SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, + SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer }, + codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret); + AVCODEC_SAMPLE_LOGI("====== AudioEncoder SetCallback ======"); + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::Configure(const SampleInfo &sampleInfo) +{ + OH_AVFormat *format = OH_AVFormat_Create(); + CHECK_AND_RETURN_RET_LOG(format != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "AVFormat create failed"); + + OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUDIO_SAMPLE_FORMAT, sampleInfo.audioSampleForamt); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_CHANNEL_COUNT, sampleInfo.audioChannelCount); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_SAMPLE_RATE, sampleInfo.audioSampleRate); + OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, sampleInfo.audioBitRate); + OH_AVFormat_SetLongValue(format, OH_MD_KEY_CHANNEL_LAYOUT, sampleInfo.audioChannelLayout); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_MAX_INPUT_SIZE, sampleInfo.audioMaxInputSize); + AVCODEC_SAMPLE_LOGI("audioChannelCount:%{public}d audioSampleRate:%{public}d audioBitRate:%{public}d " + "audioChannelLayout:%{public}ld", + sampleInfo.audioChannelCount, sampleInfo.audioSampleRate, sampleInfo.audioBitRate, + sampleInfo.audioChannelLayout); + + int ret = OH_AudioCodec_Configure(encoder_, format); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Config failed, ret: %{public}d", ret); + OH_AVFormat_Destroy(format); + format = nullptr; + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData) +{ + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + CHECK_AND_RETURN_RET_LOG(codecUserData != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Invalid param: codecUserData"); + + // Configure audio encoder + int32_t ret = Configure(sampleInfo); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Configure failed"); + + // SetCallback for audio encoder + ret = SetCallback(codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, + "Set callback failed, ret: %{public}d", ret); + + // Prepare audio encoder + { + int ret = OH_AudioCodec_Prepare(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Prepare failed, ret: %{public}d", ret); + } + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::Start() +{ + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + + int ret = OH_AudioCodec_Start(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::PushInputData(CodecBufferInfo &info) +{ + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + int32_t ret = OH_AVBuffer_SetBufferAttr(reinterpret_cast(info.buffer), &info.attr); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set avbuffer attr failed"); + ret = OH_AudioCodec_PushInputBuffer(encoder_, info.bufferIndex); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Push input data failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::FreeOutputData(uint32_t bufferIndex) +{ + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + + int32_t ret = AVCODEC_SAMPLE_ERR_OK; + ret = OH_AudioCodec_FreeOutputBuffer(encoder_, bufferIndex); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Free output data failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::Stop() +{ + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + + int ret = OH_AudioCodec_Flush(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Flush failed, ret: %{public}d", ret); + + ret = OH_AudioCodec_Stop(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Stop failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t AudioEncoder::Release() +{ + if (encoder_ != nullptr) { + OH_AudioCodec_Flush(encoder_); + OH_AudioCodec_Stop(encoder_); + OH_AudioCodec_Destroy(encoder_); + encoder_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} diff --git a/entry/src/main/cpp/capbilities/Demuxer.cpp b/entry/src/main/cpp/capbilities/Demuxer.cpp new file mode 100644 index 0000000..93de06a --- /dev/null +++ b/entry/src/main/cpp/capbilities/Demuxer.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Demuxer.h" + +#undef LOG_TAG +#define LOG_TAG "Demuxer" + +Demuxer::~Demuxer() { Release(); } + +int32_t Demuxer::Create(SampleInfo &info) { + source_ = OH_AVSource_CreateWithFD(info.inputFd, info.inputFileOffset, info.inputFileSize); + CHECK_AND_RETURN_RET_LOG(source_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, + "Create demuxer source failed, fd: %{public}d, offset: %{public}" PRId64 + ", file size: %{public}" PRId64, + info.inputFd, info.inputFileOffset, info.inputFileSize); + demuxer_ = OH_AVDemuxer_CreateWithSource(source_); + CHECK_AND_RETURN_RET_LOG(demuxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create demuxer failed"); + + auto sourceFormat = std::shared_ptr(OH_AVSource_GetSourceFormat(source_), OH_AVFormat_Destroy); + CHECK_AND_RETURN_RET_LOG(sourceFormat != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Get source format failed"); + + int32_t ret = GetTrackInfo(sourceFormat, info); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Get video track info failed"); + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Demuxer::ReadSample(int32_t trackId, OH_AVBuffer *buffer, OH_AVCodecBufferAttr &attr) { + CHECK_AND_RETURN_RET_LOG(demuxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Demuxer is null"); + int32_t ret = OH_AVDemuxer_ReadSampleBuffer(demuxer_, trackId, buffer); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Read sample failed"); + ret = OH_AVBuffer_GetBufferAttr(buffer, &attr); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "GetBufferAttr failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Demuxer::Release() { + if (demuxer_ != nullptr) { + OH_AVDemuxer_Destroy(demuxer_); + demuxer_ = nullptr; + } + if (source_ != nullptr) { + OH_AVSource_Destroy(source_); + source_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Demuxer::GetTrackInfo(std::shared_ptr sourceFormat, SampleInfo &info) { + int32_t trackCount = 0; + OH_AVFormat_GetIntValue(sourceFormat.get(), OH_MD_KEY_TRACK_COUNT, &trackCount); + for (int32_t index = 0; index < trackCount; index++) { + int trackType = -1; + auto trackFormat = + std::shared_ptr(OH_AVSource_GetTrackFormat(source_, index), OH_AVFormat_Destroy); + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_TRACK_TYPE, &trackType); + if (trackType == MEDIA_TYPE_VID) { + OH_AVDemuxer_SelectTrackByID(demuxer_, index); + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_WIDTH, &info.videoWidth); + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_HEIGHT, &info.videoHeight); + OH_AVFormat_GetDoubleValue(trackFormat.get(), OH_MD_KEY_FRAME_RATE, &info.frameRate); + OH_AVFormat_GetLongValue(trackFormat.get(), OH_MD_KEY_BITRATE, &info.bitrate); + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_ROTATION, &info.rotation); + char *videoCodecMime; + OH_AVFormat_GetStringValue(trackFormat.get(), OH_MD_KEY_CODEC_MIME, + const_cast(&videoCodecMime)); + info.videoCodecMime = videoCodecMime; + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_PROFILE, &info.hevcProfile); + videoTrackId_ = index; + + AVCODEC_SAMPLE_LOGI("====== Demuxer Video config ======"); + AVCODEC_SAMPLE_LOGI("Mime: %{public}s", videoCodecMime); + AVCODEC_SAMPLE_LOGI("%{public}d*%{public}d, %{public}.1ffps, %{public}" PRId64 "kbps", info.videoWidth, + info.videoHeight, info.frameRate, info.bitrate / 1024); + AVCODEC_SAMPLE_LOGI("====== Demuxer Video config ======"); + } else if (trackType == MEDIA_TYPE_AUD) { + OH_AVDemuxer_SelectTrackByID(demuxer_, index); + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AUDIO_SAMPLE_FORMAT, &info.audioSampleForamt); + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AUD_CHANNEL_COUNT, &info.audioChannelCount); + OH_AVFormat_GetLongValue(trackFormat.get(), OH_MD_KEY_CHANNEL_LAYOUT, &info.audioChannelLayout); + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AUD_SAMPLE_RATE, &info.audioSampleRate); + char *audioCodecMime; + OH_AVFormat_GetStringValue(trackFormat.get(), OH_MD_KEY_CODEC_MIME, + const_cast(&audioCodecMime)); + uint8_t *codecConfig = nullptr; + OH_AVFormat_GetBuffer(trackFormat.get(), OH_MD_KEY_CODEC_CONFIG, &codecConfig, &info.codecConfigLen); + if (info.codecConfigLen > 0 && info.codecConfigLen < sizeof(info.codecConfig)) { + memcpy(info.codecConfig, codecConfig, info.codecConfigLen); + AVCODEC_SAMPLE_LOGI( + "codecConfig:%{public}p, len:%{public}i, 0:0x%{public}02x 1:0x:%{public}02x, bufLen:%{public}u", + info.codecConfig, (int)info.codecConfigLen, info.codecConfig[0], info.codecConfig[1], + sizeof(info.codecConfig)); + } + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AAC_IS_ADTS, &info.aacAdts); + + info.audioCodecMime = audioCodecMime; + audioTrackId_ = index; + + AVCODEC_SAMPLE_LOGI("====== Demuxer Audio config ======"); + AVCODEC_SAMPLE_LOGI( + "audioMime:%{public}s sampleForamt:%{public}d " + "sampleRate:%{public}d channelCount:%{public}d channelLayout:%{public}d adts:%{public}i", + info.audioCodecMime.c_str(), info.audioSampleForamt, info.audioSampleRate, info.audioChannelCount, + info.audioChannelLayout, info.aacAdts); + AVCODEC_SAMPLE_LOGI("====== Demuxer Audio config ======"); + } + } + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Demuxer::GetVideoTrackId() { return videoTrackId_; } +int32_t Demuxer::GetAudioTrackId() { return audioTrackId_; } \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/Muxer.cpp b/entry/src/main/cpp/capbilities/Muxer.cpp new file mode 100644 index 0000000..5d7fee6 --- /dev/null +++ b/entry/src/main/cpp/capbilities/Muxer.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Muxer.h" +#include "dfx/error/AVCodecSampleError.h" +#include + +#undef LOG_TAG +#define LOG_TAG "Muxer" + +namespace { +constexpr int32_t CAMERA_ANGLE = 90; +constexpr int32_t SAMPLE_RATE = 16000; +} // namespace + +Muxer::~Muxer() { Release(); } + +// [Start format_path] +// Create an encapsulator instance object and set the encapsulation format to mp4 +int32_t Muxer::Create(int32_t fd) { + muxer_ = OH_AVMuxer_Create(fd, AV_OUTPUT_FORMAT_MPEG_4); + CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer create failed, fd: %{public}d", fd); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Muxer::Config(SampleInfo &sampleInfo) { + CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer is null"); + +// OH_AVFormat *formatAudio = OH_AVFormat_CreateAudioFormat(sampleInfo.audioCodecMime.data(), +// sampleInfo.audioSampleRate, sampleInfo.audioChannelCount); +// CHECK_AND_RETURN_RET_LOG(formatAudio != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create audio format failed"); +// OH_AVFormat_SetIntValue(formatAudio, OH_MD_KEY_PROFILE, AAC_PROFILE_LC); +// int32_t ret = OH_AVMuxer_AddTrack(muxer_, &audioTrackId_, formatAudio); +// OH_AVFormat_Destroy(formatAudio); + + OH_AVFormat *formatVideo = + OH_AVFormat_CreateVideoFormat(sampleInfo.outputVideoCodecMime.data(), sampleInfo.outputVideoWidth, sampleInfo.outputVideoHeight); + CHECK_AND_RETURN_RET_LOG(formatVideo != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create video format failed"); + + OH_AVFormat_SetDoubleValue(formatVideo, OH_MD_KEY_FRAME_RATE, sampleInfo.outputFrameRate); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_WIDTH, sampleInfo.outputVideoWidth); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_HEIGHT, sampleInfo.outputVideoHeight); + OH_AVFormat_SetStringValue(formatVideo, OH_MD_KEY_CODEC_MIME, sampleInfo.outputVideoCodecMime.data()); + if (sampleInfo.isHDRVivid) { + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_VIDEO_IS_HDR_VIVID, 1); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_RANGE_FLAG, sampleInfo.rangFlag); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_COLOR_PRIMARIES, sampleInfo.primary); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_TRANSFER_CHARACTERISTICS, sampleInfo.transfer); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_MATRIX_COEFFICIENTS, sampleInfo.matrix); + } + + int32_t ret = OH_AVMuxer_AddTrack(muxer_, &videoTrackId_, formatVideo); + OH_AVFormat_Destroy(formatVideo); + formatVideo = nullptr; + OH_AVMuxer_SetRotation(muxer_, CAMERA_ANGLE); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "AddTrack failed"); + return AVCODEC_SAMPLE_ERR_OK; +} +// [End format_path] + +int32_t Muxer::Start() { + CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer is null"); + + int ret = OH_AVMuxer_Start(muxer_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Muxer::WriteSample(int32_t trackId, OH_AVBuffer *buffer, OH_AVCodecBufferAttr &attr){ + std::lock_guard lock(writeMutex_); + + CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer is null"); + CHECK_AND_RETURN_RET_LOG(buffer != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Get a empty buffer"); + + int32_t ret = OH_AVBuffer_SetBufferAttr(buffer, &attr); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "SetBufferAttr failed"); + + ret = OH_AVMuxer_WriteSampleBuffer(muxer_, trackId, buffer); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Write sample failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Muxer::Release() { + if (muxer_ != nullptr) { + OH_AVMuxer_Destroy(muxer_); + muxer_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Muxer::GetVideoTrackId() { return videoTrackId_; } +int32_t Muxer::GetAudioTrackId() { return audioTrackId_; } \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/VideoDecoder.cpp b/entry/src/main/cpp/capbilities/VideoDecoder.cpp new file mode 100644 index 0000000..f832255 --- /dev/null +++ b/entry/src/main/cpp/capbilities/VideoDecoder.cpp @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VideoDecoder.h" + +#undef LOG_TAG +#define LOG_TAG "VideoDecoder" + +namespace { +constexpr int LIMIT_LOGD_FREQUENCY = 50; +} // namespace + +VideoDecoder::~VideoDecoder() { Release(); } + +// Development using the system codec AVCodec +// Create a decoder instance object +int32_t VideoDecoder::Create(const std::string &videoCodecMime) { + decoder_ = OH_VideoDecoder_CreateByMime(videoCodecMime.c_str()); + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +// Setting the callback function +int32_t VideoDecoder::SetCallback(CodecUserData *codecUserData) { + int32_t ret = AV_ERR_OK; + ret = OH_VideoDecoder_RegisterCallback(decoder_, + {SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, + SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer}, + codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret); + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoDecoder::Configure(const SampleInfo &sampleInfo) { + // [StartExclude set_decoder] + OH_AVFormat *format = OH_AVFormat_Create(); + CHECK_AND_RETURN_RET_LOG(format != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "AVFormat create failed"); + + OH_AVFormat_SetIntValue(format, OH_MD_KEY_WIDTH, sampleInfo.videoWidth); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_HEIGHT, sampleInfo.videoHeight); + OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, sampleInfo.frameRate); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_PIXEL_FORMAT, sampleInfo.pixelFormat); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_ROTATION, sampleInfo.rotation); + + AVCODEC_SAMPLE_LOGI("====== VideoDecoder config ======"); + AVCODEC_SAMPLE_LOGI("%{public}d*%{public}d, %{public}.1ffps", sampleInfo.videoWidth, sampleInfo.videoHeight, + sampleInfo.frameRate); + AVCODEC_SAMPLE_LOGI("====== VideoDecoder config ======"); + // [EndExclude set_decoder] + int ret = OH_VideoDecoder_Configure(decoder_, format); + // [StartExclude set_decoder] + OH_AVFormat_Destroy(format); + format = nullptr; + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Config failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; + // [EndExclude set_decoder] +} + + +int32_t VideoDecoder::Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData) { + // [StartExclude decoder_ready] + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + CHECK_AND_RETURN_RET_LOG(codecUserData != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Invalid param: codecUserData"); + + // Configure video decoder + int32_t ret = Configure(sampleInfo); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Configure failed"); + // SetSurface from video decoder +// if (sampleInfo.window != nullptr) { +// int ret = OH_VideoDecoder_SetSurface(decoder_, sampleInfo.window); +// CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK && sampleInfo.window, AVCODEC_SAMPLE_ERR_ERROR, +// "Set surface failed, ret: %{public}d", ret); +// } + + // SetCallback for video decoder + ret = SetCallback(codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, + "Set callback failed, ret: %{public}d", ret); + + // Prepare video decoder + { + int ret = OH_VideoDecoder_Prepare(decoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Prepare failed, ret: %{public}d", ret); + } + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoDecoder::Start() { + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + + int ret = OH_VideoDecoder_Start(decoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoDecoder::PushInputBuffer(CodecBufferInfo &info) { + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + int32_t ret = OH_VideoDecoder_PushInputBuffer(decoder_, info.bufferIndex); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Push input data failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoDecoder::FreeOutputBuffer(uint32_t bufferIndex, bool render) { + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + + int32_t ret = AVCODEC_SAMPLE_ERR_OK; + if (render) { + ret = OH_VideoDecoder_RenderOutputBuffer(decoder_, bufferIndex); + } else { + ret = OH_VideoDecoder_FreeOutputBuffer(decoder_, bufferIndex); + } + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Free output data failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +// Destroy the decoder instance and release resources +int32_t VideoDecoder::Release() { + if (decoder_ != nullptr) { + OH_VideoDecoder_Flush(decoder_); + OH_VideoDecoder_Stop(decoder_); + OH_VideoDecoder_Destroy(decoder_); + decoder_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/VideoEncoder.cpp b/entry/src/main/cpp/capbilities/VideoEncoder.cpp new file mode 100644 index 0000000..b45e0fd --- /dev/null +++ b/entry/src/main/cpp/capbilities/VideoEncoder.cpp @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VideoEncoder.h" + +#undef LOG_TAG +#define LOG_TAG "VideoEncoder" + +VideoEncoder::~VideoEncoder() { Release(); } + +// [Start encoder_initialization] +// Create a video coder and initialize it +int32_t VideoEncoder::Create(const std::string &videoCodecMime) { + encoder_ = OH_VideoEncoder_CreateByMime(videoCodecMime.c_str()); + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed"); + return AVCODEC_SAMPLE_ERR_OK; +} +// [End encoder_initialization] + +int32_t VideoEncoder::Config(SampleInfo &sampleInfo, CodecUserData *codecUserData) { + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + CHECK_AND_RETURN_RET_LOG(codecUserData != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Invalid param: codecUserData"); + + // Configure video encoder + int32_t ret = Configure(sampleInfo); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Configure failed"); + + // GetSurface from video encoder +// ret = GetSurface(sampleInfo); +// CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Get surface failed"); + + // SetCallback for video encoder + ret = SetCallback(codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, + "Set callback failed, ret: %{public}d", ret); + + // Prepare video encoder + ret = OH_VideoEncoder_Prepare(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Prepare failed, ret: %{public}d", ret); + + return AVCODEC_SAMPLE_ERR_OK; +} + +// [Start start_encoder] +// Start Encoder +int32_t VideoEncoder::Start() { + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + + int ret = OH_VideoEncoder_Start(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} +// [End start_encoder] + +int32_t VideoEncoder::FreeOutputBuffer(uint32_t bufferIndex) { + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + + int32_t ret = OH_VideoEncoder_FreeOutputBuffer(encoder_, bufferIndex); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Free output data failed, ret: %{public}d", + ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoEncoder::NotifyEndOfStream() { + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + + int32_t ret = OH_VideoEncoder_NotifyEndOfStream(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Notify end of stream failed, ret: %{public}d", + ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoEncoder::Stop() { + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); + + int ret = OH_VideoEncoder_Flush(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Flush failed, ret: %{public}d", ret); + + ret = OH_VideoEncoder_Stop(encoder_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Stop failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoEncoder::Release() { + if (encoder_ != nullptr) { + OH_VideoEncoder_Destroy(encoder_); + encoder_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoEncoder::SetCallback(CodecUserData *codecUserData) { + int32_t ret = + OH_VideoEncoder_RegisterCallback(encoder_, + {SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, + SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer}, + codecUserData); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret); + + return AVCODEC_SAMPLE_ERR_OK; +} + +// Camera+AVCodec +// [Start camera_AVCodec] +int32_t VideoEncoder::Configure(const SampleInfo &sampleInfo) { + // [StartExclude camera_AVCodec] + OH_AVFormat *format = OH_AVFormat_Create(); + CHECK_AND_RETURN_RET_LOG(format != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "AVFormat create failed"); + + OH_AVFormat_SetIntValue(format, OH_MD_KEY_WIDTH, sampleInfo.videoWidth); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_HEIGHT, sampleInfo.videoHeight); + OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, sampleInfo.frameRate); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_PIXEL_FORMAT, sampleInfo.pixelFormat); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_VIDEO_ENCODE_BITRATE_MODE, sampleInfo.bitrateMode); + OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, sampleInfo.bitrate); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_PROFILE, sampleInfo.hevcProfile); + // [EndExclude camera_AVCodec] + // Setting HDRVivid-related parameters + if (sampleInfo.isHDRVivid) { + OH_AVFormat_SetIntValue(format, OH_MD_KEY_I_FRAME_INTERVAL, sampleInfo.iFrameInterval); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_RANGE_FLAG, sampleInfo.rangFlag); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_COLOR_PRIMARIES, sampleInfo.primary); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_TRANSFER_CHARACTERISTICS, sampleInfo.transfer); + OH_AVFormat_SetIntValue(format, OH_MD_KEY_MATRIX_COEFFICIENTS, sampleInfo.matrix); + } + // [StartExclude camera_AVCodec] + AVCODEC_SAMPLE_LOGI("====== VideoEncoder config ======"); + AVCODEC_SAMPLE_LOGI("%{public}d*%{public}d, %{public}.1ffps", sampleInfo.videoWidth, sampleInfo.videoHeight, + sampleInfo.frameRate); + // 1024: ratio of kbps to bps + AVCODEC_SAMPLE_LOGI("BitRate Mode: %{public}d, BitRate: %{public}" PRId64 "kbps", sampleInfo.bitrateMode, + sampleInfo.bitrate / 1024); + AVCODEC_SAMPLE_LOGI("====== VideoEncoder config ======"); + + // [Start set_encoder] + // Setting the Encoder + int ret = OH_VideoEncoder_Configure(encoder_, format); + // [End set_encoder] + OH_AVFormat_Destroy(format); + format = nullptr; + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Config failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; + // [EndExclude camera_AVCodec] +} +// [End camera_AVCodec] + +int32_t VideoEncoder::GetSurface(SampleInfo &sampleInfo) { + int32_t ret = OH_VideoEncoder_GetSurface(encoder_, &sampleInfo.window); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK && sampleInfo.window, AVCODEC_SAMPLE_ERR_ERROR, + "Get surface failed, ret: %{public}d", ret); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t VideoEncoder::PushInputBuffer(CodecBufferInfo &info) { + CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); + int32_t ret = OH_VideoEncoder_PushInputBuffer(encoder_, info.bufferIndex); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Push input data failed"); + return AVCODEC_SAMPLE_ERR_OK; +} \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/include/AudioCapturer.h b/entry/src/main/cpp/capbilities/include/AudioCapturer.h new file mode 100644 index 0000000..d9e681a --- /dev/null +++ b/entry/src/main/cpp/capbilities/include/AudioCapturer.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AVCODECVIDEO_AUDIOCAPTURER_H +#define AVCODECVIDEO_AUDIOCAPTURER_H + +#include +#include +#include + +#include "SampleInfo.h" + +class AudioCapturer { +public: + AudioCapturer() = default; + ~AudioCapturer(); + + void AudioCapturerInit(SampleInfo& sampleInfo, CodecUserData *audioEncContext); + void AudioCapturerStart(); + void AudioCapturerRelease(); + +private: + OH_AudioCapturer *audioCapturer_ = nullptr; + OH_AudioStreamBuilder *builder_ = nullptr; +}; + + +#endif //AVCODECVIDEO_AUDIOCAPTURER_H diff --git a/entry/src/main/cpp/capbilities/include/AudioDecoder.h b/entry/src/main/cpp/capbilities/include/AudioDecoder.h new file mode 100644 index 0000000..5c77684 --- /dev/null +++ b/entry/src/main/cpp/capbilities/include/AudioDecoder.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AUDIODECODER_H +#define AUDIODECODER_H + +#include "multimedia/player_framework/native_avcodec_audiocodec.h" +#include "multimedia/player_framework/native_avbuffer_info.h" +#include "SampleCallback.h" +#include "AVCodecSampleLog.h" + +class AudioDecoder { +public: + AudioDecoder() = default; + ~AudioDecoder(); + + int32_t Create(const std::string &codecMime); + int32_t Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData); + int32_t Start(); + int32_t PushInputBuffer(CodecBufferInfo &info); + int32_t FreeOutputBuffer(uint32_t bufferIndex, bool render); + int32_t Release(); + +private: + int32_t SetCallback(CodecUserData *codecUserData); + int32_t Configure(const SampleInfo &sampleInfo); + + bool isAVBufferMode_ = false; + OH_AVCodec *decoder_; +}; +#endif // AUDIODECODER_H \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/include/AudioEncoder.h b/entry/src/main/cpp/capbilities/include/AudioEncoder.h new file mode 100644 index 0000000..1261bf1 --- /dev/null +++ b/entry/src/main/cpp/capbilities/include/AudioEncoder.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AVCODECVIDEO_AUDIOENCODER_H +#define AVCODECVIDEO_AUDIOENCODER_H + +#include "multimedia/player_framework/native_avcodec_audiocodec.h" +#include "multimedia/player_framework/native_avbuffer_info.h" +#include "multimedia/native_audio_channel_layout.h" +#include "SampleInfo.h" +#include "SampleCallback.h" +#include "dfx/error/AVCodecSampleError.h" +#include "dfx/log/AVCodecSampleLog.h" + +class AudioEncoder { +public: + AudioEncoder() = default; + ~AudioEncoder(); + + int32_t Create(const std::string &codecMime); + int32_t Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData); + int32_t Start(); + int32_t PushInputData(CodecBufferInfo &info); + int32_t FreeOutputData(uint32_t bufferIndex); + int32_t Stop(); + int32_t Release(); + +private: + int32_t SetCallback(CodecUserData *codecUserData); + int32_t Configure(const SampleInfo &sampleInfo); + + bool isAVBufferMode_ = false; + OH_AVCodec *encoder_; +}; + +#endif // AVCODECVIDEO_AUDIOENCODER_H diff --git a/entry/src/main/cpp/capbilities/include/Demuxer.h b/entry/src/main/cpp/capbilities/include/Demuxer.h new file mode 100644 index 0000000..26fcd6c --- /dev/null +++ b/entry/src/main/cpp/capbilities/include/Demuxer.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef DEMUXER_H +#define DEMUXER_H + +#include +#include "napi/native_api.h" +#include "multimedia/player_framework/native_avdemuxer.h" +#include "SampleInfo.h" +#include "dfx/error/AVCodecSampleError.h" +#include "AVCodecSampleLog.h" + +class Demuxer { +public: + Demuxer() = default; + ~Demuxer(); + int32_t Create(SampleInfo &sampleInfo); + int32_t ReadSample(int32_t trackId, OH_AVBuffer *buffer, OH_AVCodecBufferAttr &attr); + int32_t Release(); + int32_t GetVideoTrackId(); + int32_t GetAudioTrackId(); + +private: + int32_t GetTrackInfo(std::shared_ptr sourceFormat, SampleInfo &info); + + OH_AVSource *source_; + OH_AVDemuxer *demuxer_; + int32_t videoTrackId_; + int32_t audioTrackId_; +}; + +#endif // DEMUXER_H \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/include/Muxer.h b/entry/src/main/cpp/capbilities/include/Muxer.h new file mode 100644 index 0000000..e9a040a --- /dev/null +++ b/entry/src/main/cpp/capbilities/include/Muxer.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef MUXER_H +#define MUXER_H + +#include "AVCodecSampleLog.h" +#include "SampleInfo.h" +#include "multimedia/player_framework/native_avmuxer.h" +#include + +class Muxer { +public: + Muxer() = default; + ~Muxer(); + + int32_t Create(int32_t fd); + int32_t Config(SampleInfo &sampleInfo); + int32_t Start(); + int32_t WriteSample(int32_t trackId, OH_AVBuffer *buffer, OH_AVCodecBufferAttr &attr); + int32_t Release(); + int32_t GetVideoTrackId(); + int32_t GetAudioTrackId(); + +private: + OH_AVMuxer *muxer_ = nullptr; + int32_t videoTrackId_ = -1; + int32_t audioTrackId_ = -1; + std::mutex writeMutex_; +}; + +#endif // MUXER_H \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/include/VideoDecoder.h b/entry/src/main/cpp/capbilities/include/VideoDecoder.h new file mode 100644 index 0000000..45ee990 --- /dev/null +++ b/entry/src/main/cpp/capbilities/include/VideoDecoder.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef VIDEODECODER_H +#define VIDEODECODER_H + +#include "multimedia/player_framework/native_avcodec_videodecoder.h" +#include "multimedia/player_framework/native_avbuffer_info.h" +#include "SampleInfo.h" +#include "SampleCallback.h" +#include "dfx/error/AVCodecSampleError.h" +#include "AVCodecSampleLog.h" + +class VideoDecoder { +public: + VideoDecoder() = default; + ~VideoDecoder(); + + int32_t Create(const std::string &videoCodecMime); + int32_t Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData); + int32_t PushInputBuffer(CodecBufferInfo &info); + int32_t FreeOutputBuffer(uint32_t bufferIndex, bool render); + int32_t Start(); + int32_t Release(); + +private: + int32_t SetCallback(CodecUserData *codecUserData); + int32_t Configure(const SampleInfo &sampleInfo); + + bool isAVBufferMode_ = false; + OH_AVCodec *decoder_; +}; +#endif // VIDEODECODER_H \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/include/VideoEncoder.h b/entry/src/main/cpp/capbilities/include/VideoEncoder.h new file mode 100644 index 0000000..4d511d3 --- /dev/null +++ b/entry/src/main/cpp/capbilities/include/VideoEncoder.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef VIDEOENCODER_H +#define VIDEOENCODER_H + +#include "multimedia/player_framework/native_avcodec_videoencoder.h" +#include "multimedia/player_framework/native_avbuffer_info.h" +#include "SampleInfo.h" +#include "native_window/external_window.h" +#include "native_window/buffer_handle.h" +#include "SampleCallback.h" +#include "dfx/error/AVCodecSampleError.h" +#include "AVCodecSampleLog.h" + +class VideoEncoder { +public: + VideoEncoder() = default; + ~VideoEncoder(); + + int32_t Create(const std::string &videoCodecMime); + int32_t Config(SampleInfo &sampleInfo, CodecUserData *codecUserData); + int32_t Start(); + int32_t FreeOutputBuffer(uint32_t bufferIndex); + int32_t PushInputBuffer(CodecBufferInfo &info); + int32_t NotifyEndOfStream(); + int32_t Stop(); + int32_t Release(); + +private: + int32_t SetCallback(CodecUserData *codecUserData); + int32_t Configure(const SampleInfo &sampleInfo); + int32_t GetSurface(SampleInfo &sampleInfo); + bool isAVBufferMode_ = false; + OH_AVCodec *encoder_ = nullptr; +}; +#endif // VIDEOENCODER_H \ No newline at end of file diff --git a/entry/src/main/cpp/common/SampleCallback.cpp b/entry/src/main/cpp/common/SampleCallback.cpp new file mode 100644 index 0000000..82d550e --- /dev/null +++ b/entry/src/main/cpp/common/SampleCallback.cpp @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SampleCallback.h" +#include "AVCodecSampleLog.h" +#include "multimedia/player_framework/native_avcodec_videodecoder.h" + +namespace { +constexpr int LIMIT_LOGD_FREQUENCY = 50; +} + +// Custom write data function +int32_t SampleCallback::OnRenderWriteData(OH_AudioRenderer *renderer, void *userData, void *buffer, int32_t length) { + (void)renderer; + (void)length; + CodecUserData *codecUserData = static_cast(userData); + + // Write the data to be played to the buffer by length + uint8_t *dest = (uint8_t *)buffer; + size_t index = 0; + std::unique_lock lock(codecUserData->outputMutex); + // Retrieve the length of the data to be played from the queue + while (!codecUserData->renderQueue.empty() && index < length) { + dest[index++] = codecUserData->renderQueue.front(); + codecUserData->renderQueue.pop(); + } + AVCODEC_SAMPLE_LOGD("render BufferLength:%{public}d Out buffer count: %{public}u, renderQueue.size: %{public}u " + "renderReadSize: %{public}u", + length, codecUserData->outputFrameCount, (unsigned int)codecUserData->renderQueue.size(), + (unsigned int)index); + if (codecUserData->renderQueue.size() < length) { + codecUserData->renderCond.notify_all(); + } + return 0; +} +// Customize the audio stream event function +int32_t SampleCallback::OnRenderStreamEvent(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Event event) { + (void)renderer; + (void)userData; + (void)event; + // Update the player status and interface based on the audio stream event information represented by the event + return 0; +} +// Customize the audio interrupt event function +int32_t SampleCallback::OnRenderInterruptEvent(OH_AudioRenderer *renderer, void *userData, + OH_AudioInterrupt_ForceType type, OH_AudioInterrupt_Hint hint) { + (void)renderer; + (void)userData; + (void)type; + (void)hint; + // Update the player status and interface based on the audio interrupt information indicated by type and hint + return 0; +} +// Custom exception callback functions +int32_t SampleCallback::OnRenderError(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Result error) { + (void)renderer; + (void)userData; + (void)error; + AVCODEC_SAMPLE_LOGE("OnRenderError"); + // Handle the audio exception information based on the error message + return 0; +} + +void SampleCallback::OnCodecError(OH_AVCodec *codec, int32_t errorCode, void *userData) { + (void)codec; + (void)errorCode; + (void)userData; + AVCODEC_SAMPLE_LOGI("On codec error, error code: %{public}d", errorCode); +} + +void SampleCallback::OnCodecFormatChange(OH_AVCodec *codec, OH_AVFormat *format, void *userData) { + AVCODEC_SAMPLE_LOGI("On codec format change"); +} + +void SampleCallback::OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) { + if (userData == nullptr) { + return; + } + (void)codec; + + CodecUserData *codecUserData = static_cast(userData); + if (codecUserData->isEncFirstFrame) { + OH_AVFormat *format = OH_VideoDecoder_GetOutputDescription(codec); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &codecUserData->width); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &codecUserData->height); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_STRIDE, &codecUserData->widthStride); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_SLICE_HEIGHT, &codecUserData->heightStride); + OH_AVFormat_Destroy(format); + codecUserData->isEncFirstFrame = false; + } + std::unique_lock lock(codecUserData->inputMutex); + codecUserData->inputBufferInfoQueue.emplace(index, buffer); + codecUserData->inputCond.notify_all(); +} + +void SampleCallback::OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) { + if (userData == nullptr) { + return; + } + (void)codec; + CodecUserData *codecUserData = static_cast(userData); + if(codecUserData->isDecFirstFrame) { + OH_AVFormat *format = OH_VideoDecoder_GetOutputDescription(codec); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &codecUserData->width); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &codecUserData->height); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_STRIDE, &codecUserData->widthStride); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_SLICE_HEIGHT, &codecUserData->heightStride); + OH_AVFormat_Destroy(format); + codecUserData->isDecFirstFrame = false; + } + std::unique_lock lock(codecUserData->outputMutex); + codecUserData->outputBufferInfoQueue.emplace(index, buffer); + codecUserData->outputCond.notify_all(); +} \ No newline at end of file diff --git a/entry/src/main/cpp/common/SampleCallback.h b/entry/src/main/cpp/common/SampleCallback.h new file mode 100644 index 0000000..4439deb --- /dev/null +++ b/entry/src/main/cpp/common/SampleCallback.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AVCODEC_SAMPLE_CALLBACK_H +#define AVCODEC_SAMPLE_CALLBACK_H + +#include +#include +#include "SampleInfo.h" + +class SampleCallback { +public: + static int32_t OnRenderWriteData(OH_AudioRenderer *renderer, void *userData, void *buffer, int32_t length); + static int32_t OnRenderStreamEvent(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Event event); + static int32_t OnRenderInterruptEvent(OH_AudioRenderer *renderer, void *userData, OH_AudioInterrupt_ForceType type, + OH_AudioInterrupt_Hint hint); + static int32_t OnRenderError(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Result error); + + static void OnCodecError(OH_AVCodec *codec, int32_t errorCode, void *userData); + static void OnCodecFormatChange(OH_AVCodec *codec, OH_AVFormat *format, void *userData); + static void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); + static void OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); + static void DecOnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); +}; + +#endif // AVCODEC_SAMPLE_CALLBACK_H \ No newline at end of file diff --git a/entry/src/main/cpp/common/SampleInfo.h b/entry/src/main/cpp/common/SampleInfo.h new file mode 100644 index 0000000..7274246 --- /dev/null +++ b/entry/src/main/cpp/common/SampleInfo.h @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AVCODEC_SAMPLE_INFO_H +#define AVCODEC_SAMPLE_INFO_H + +#include +#include +#include +#include +#include +#include +#include "multimedia/player_framework/native_avcodec_base.h" +#include "multimedia/player_framework/native_avbuffer.h" + +const std::string_view MIME_VIDEO_AVC = "video/avc"; +const std::string_view MIME_VIDEO_HEVC = "video/hevc"; +const std::string_view MIME_AUDIO_MPEG = "audio/mpeg"; + +constexpr int32_t BITRATE_10M = 10 * 1024 * 1024; // 10Mbps +constexpr int32_t BITRATE_20M = 20 * 1024 * 1024; // 20Mbps +constexpr int32_t BITRATE_30M = 30 * 1024 * 1024; // 30Mbps + +struct SampleInfo { + int32_t inputFd = -1; + int32_t outputFd = -1; + int64_t inputFileOffset = 0; + int64_t inputFileSize = 0; + std::string inputFilePath; + std::string videoCodecMime = ""; + std::string audioCodecMime = ""; + int32_t videoWidth = 0; + int32_t videoHeight = 0; + + int32_t outputVideoWidth = 0; + int32_t outputVideoHeight = 0; + double outputFrameRate = 0.0; + std::string outputVideoCodecMime = ""; + int64_t outputBitrate = 10 * 1024 * 1024; // 10Mbps; + + double frameRate = 0.0; + int64_t bitrate = 10 * 1024 * 1024; // 10Mbps; + int64_t frameInterval = 0; + OH_AVPixelFormat pixelFormat = AV_PIXEL_FORMAT_NV12; + uint32_t bitrateMode = CBR; + int32_t iFrameInterval = 100; + int32_t rangFlag = 1; + + int32_t audioSampleForamt = 0; + int32_t audioSampleRate = 0; + int32_t audioChannelCount = 0; + int64_t audioChannelLayout = 0; + int32_t audioBitRate = 0; + uint8_t audioCodecConfig[100] = { 0 }; + size_t audioCodecSize = 0; + int32_t audioMaxInputSize = 0; + OH_AVFormat *audioFormat; + + + int32_t isHDRVivid = 0; + int32_t hevcProfile = HEVC_PROFILE_MAIN; + OH_ColorPrimary primary = COLOR_PRIMARY_BT2020; + OH_TransferCharacteristic transfer = TRANSFER_CHARACTERISTIC_PQ; + OH_MatrixCoefficient matrix = MATRIX_COEFFICIENT_BT2020_CL; + + int32_t rotation = 0; + OHNativeWindow *window = nullptr; + + void (*playDoneCallback)(void *context) = nullptr; + void *playDoneCallbackData = nullptr; + uint8_t codecConfig[1024]; + size_t codecConfigLen = 0; + int32_t aacAdts = -1; +}; + +struct CodecBufferInfo { + uint32_t bufferIndex = 0; + uintptr_t *buffer = nullptr; + uint8_t *bufferAddr = nullptr; + OH_AVCodecBufferAttr attr = {0, 0, 0, AVCODEC_BUFFER_FLAGS_NONE}; + + explicit CodecBufferInfo(uint8_t *addr) : bufferAddr(addr){}; + CodecBufferInfo(uint8_t *addr, int32_t bufferSize) + : bufferAddr(addr), attr({0, bufferSize, 0, AVCODEC_BUFFER_FLAGS_NONE}){}; + CodecBufferInfo(uint32_t argBufferIndex, OH_AVBuffer *argBuffer) + : bufferIndex(argBufferIndex), buffer(reinterpret_cast(argBuffer)) + { + OH_AVBuffer_GetBufferAttr(argBuffer, &attr); + }; +}; + +struct CodecUserData { +public: + SampleInfo *sampleInfo = nullptr; + + uint32_t inputFrameCount = 0; + std::mutex inputMutex; + std::condition_variable inputCond; + std::queue inputBufferInfoQueue; + + uint32_t outputFrameCount = 0; + std::mutex outputMutex; + std::condition_variable outputCond; + std::mutex renderMutex; + std::condition_variable renderCond; + std::queue outputBufferInfoQueue; + + std::queue renderQueue; + + int32_t width = 0; + int32_t height = 0; + int32_t widthStride = 0; + int32_t heightStride = 0; + bool isEncFirstFrame = true; + bool isDecFirstFrame = true; + + // Create cache + std::vector cache; + int32_t remainlen = 0; + + void ClearCache() + { + cache.clear(); + remainlen = 0; + } + + void WriteCache(void *buffer, int32_t bufferLen) + { + if (bufferLen + remainlen > cache.size()) { + cache.resize(remainlen + bufferLen); + } + std::memcpy(cache.data() + remainlen, buffer, bufferLen); + remainlen += bufferLen; + } + + bool ReadCache(void *buffer, int32_t bufferLen) + { + if (remainlen < bufferLen) { + return false; + } + std::memcpy(buffer, cache.data(), bufferLen); + remainlen = remainlen - bufferLen; + if (remainlen > 0) { + std::memmove(cache.data(), cache.data() + bufferLen, remainlen); + } + return true; + } +}; + +#endif // AVCODEC_SAMPLE_INFO_H \ No newline at end of file diff --git a/entry/src/main/cpp/common/dfx/error/AVCodecSampleError.h b/entry/src/main/cpp/common/dfx/error/AVCodecSampleError.h new file mode 100644 index 0000000..a219a31 --- /dev/null +++ b/entry/src/main/cpp/common/dfx/error/AVCodecSampleError.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AVCODEC_SAMPLE_ERROE_H +#define AVCODEC_SAMPLE_ERROE_H + +enum AVCodecSampleError : int { + AVCODEC_SAMPLE_ERR_OK = 0, + AVCODEC_SAMPLE_ERR_ERROR = -1, +}; + +#endif // AVCODEC_SAMPLE_ERROE_H \ No newline at end of file diff --git a/entry/src/main/cpp/common/dfx/log/AVCodecSampleLog.h b/entry/src/main/cpp/common/dfx/log/AVCodecSampleLog.h new file mode 100644 index 0000000..d1b5add --- /dev/null +++ b/entry/src/main/cpp/common/dfx/log/AVCodecSampleLog.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AVCODEC_SAMPLE_LOG_H +#define AVCODEC_SAMPLE_LOG_H + +#include +#include + +#undef LOG_DOMAIN +#define LOG_DOMAIN 0x0002B66 + +#define AVCODEC_SAMPLE_LOG_FREQ_LIMIT(frequency) \ + if (1) { \ + thread_local uint64_t currentTimes = 0; \ + if (currentTimes++ % ((uint64_t)(frequency)) != 0) { \ + break; \ + } \ + } + +#define AVCODEC_SAMPLE_LOG(func, fmt, args...) \ + do { \ + (void)func(LOG_APP, "{%{public}s():%{public}d} " fmt, __FUNCTION__, __LINE__, ##args); \ + } while (0) + +#define AVCODEC_SAMPLE_LOGF(fmt, ...) AVCODEC_SAMPLE_LOG(OH_LOG_FATAL, fmt, ##__VA_ARGS__) +#define AVCODEC_SAMPLE_LOGE(fmt, ...) AVCODEC_SAMPLE_LOG(OH_LOG_ERROR, fmt, ##__VA_ARGS__) +#define AVCODEC_SAMPLE_LOGW(fmt, ...) AVCODEC_SAMPLE_LOG(OH_LOG_WARN, fmt, ##__VA_ARGS__) +#define AVCODEC_SAMPLE_LOGI(fmt, ...) AVCODEC_SAMPLE_LOG(OH_LOG_INFO, fmt, ##__VA_ARGS__) +#define AVCODEC_SAMPLE_LOGD(fmt, ...) AVCODEC_SAMPLE_LOG(OH_LOG_DEBUG, fmt, ##__VA_ARGS__) +#define AVCODEC_SAMPLE_LOGD_LIMIT(frequency, fmt, ...) \ + do { \ + AVCODEC_SAMPLE_LOG_FREQ_LIMIT(frequency); \ + AVCODEC_SAMPLE_LOGD(fmt, ##__VA_ARGS__); \ + } while (0) + +#define CHECK_AND_RETURN_RET_LOG(cond, ret, fmt, ...) \ + do { \ + if (!(cond)) { \ + AVCODEC_SAMPLE_LOGE(fmt, ##__VA_ARGS__); \ + return ret; \ + } \ + } while (0) + +#define CHECK_AND_RETURN_LOG(cond, fmt, ...) \ + do { \ + if (!(cond)) { \ + AVCODEC_SAMPLE_LOGE(fmt, ##__VA_ARGS__); \ + return; \ + } \ + } while (0) + +#define CHECK_AND_BREAK_LOG(cond, fmt, ...) \ + if (1) { \ + if (!(cond)) { \ + AVCODEC_SAMPLE_LOGW(fmt, ##__VA_ARGS__); \ + break; \ + } \ + } else \ + void(0) + +#define CHECK_AND_CONTINUE_LOG(cond, fmt, ...) \ + if (1) { \ + if (!(cond)) { \ + AVCODEC_SAMPLE_LOGW(fmt, ##__VA_ARGS__); \ + continue; \ + } \ + } else \ + void(0) + +#endif // AVCODEC_SAMPLE_LOG_H \ No newline at end of file diff --git a/entry/src/main/cpp/sample/player/Player.cpp b/entry/src/main/cpp/sample/player/Player.cpp new file mode 100644 index 0000000..9a6f282 --- /dev/null +++ b/entry/src/main/cpp/sample/player/Player.cpp @@ -0,0 +1,592 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Player.h" +#include "AVCodecSampleLog.h" +#include "dfx/error/AVCodecSampleError.h" +#include "multimedia/player_framework/native_avbuffer.h" +#include + +#undef LOG_TAG +#define LOG_TAG "player" + +namespace { +constexpr int BALANCE_VALUE = 2; +using namespace std::chrono_literals; +constexpr int8_t YUV420_SAMPLE_RATIO = 2; +} // namespace + +Player::~Player() { Player::StartDecRelease(); } + +int32_t Player::CreateAudioDecoder() { + AVCODEC_SAMPLE_LOGW("audio mime:%{public}s", sampleInfo_.audioCodecMime.c_str()); + int32_t ret = audioDecoder_->Create(sampleInfo_.audioCodecMime); + return ret; +} + +int32_t Player::CreateVideoDecoder() { + AVCODEC_SAMPLE_LOGW("video mime:%{public}s", sampleInfo_.videoCodecMime.c_str()); + int32_t ret = videoDecoder_->Create(sampleInfo_.videoCodecMime); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGW("Create video decoder failed, mime:%{public}s", sampleInfo_.videoCodecMime.c_str()); + } else { + videoDecContext_ = new CodecUserData; + ret = videoDecoder_->Config(sampleInfo_, videoDecContext_); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Video Decoder config failed"); + } + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Player::Init(SampleInfo &sampleInfo) { + std::unique_lock lock(mutex_); + CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr && audioDecoder_ == nullptr, + AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + + sampleInfo_ = sampleInfo; + + int32_t ret = InitDecoder(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Create video decoder failed"); + return AVCODEC_SAMPLE_ERR_ERROR; + } + + ret = InitEncoder(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Create video encoder failed"); + doneCond_.notify_all(); + lock.unlock(); + StartDecRelease(); + return AVCODEC_SAMPLE_ERR_ERROR; + } + + AVCODEC_SAMPLE_LOGI("Succeed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Player::InitDecoder() { + CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr && audioDecoder_ == nullptr, + AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + + videoDecoder_ = std::make_unique(); + audioDecoder_ = std::make_unique(); + demuxer_ = std::make_unique(); + + isReleased_ = false; + int32_t ret = demuxer_->Create(sampleInfo_); +// if (ret == AVCODEC_SAMPLE_ERR_OK) { +// ret = CreateAudioDecoder(); +// } else { +// AVCODEC_SAMPLE_LOGE("Create demuxer failed"); +// } + + if (ret == AVCODEC_SAMPLE_ERR_OK) { + ret = CreateVideoDecoder(); + } else { + AVCODEC_SAMPLE_LOGE("Create audio decoder failed"); + } + return ret; +} + +int32_t Player::CreateAudioEncoder() { + int32_t ret = audioEncoder_->Create(sampleInfo_.audioCodecMime); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create audio encoder(%{public}s) failed", + sampleInfo_.audioCodecMime.c_str()); + AVCODEC_SAMPLE_LOGI("Create audio encoder(%{public}s)", sampleInfo_.audioCodecMime.c_str()); + + audioEncContext_ = new CodecUserData; + ret = audioEncoder_->Config(sampleInfo_, audioEncContext_); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder config failed"); + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Player::CreateVideoEncoder() { + int32_t ret = videoEncoder_->Create(sampleInfo_.videoCodecMime); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed"); + + videoEncContext_ = new CodecUserData; + ret = videoEncoder_->Config(sampleInfo_, videoEncContext_); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder config failed"); + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Player::InitEncoder() { + CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + CHECK_AND_RETURN_RET_LOG(muxer_ == nullptr && videoEncoder_ == nullptr && audioEncoder_ == nullptr, + AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + + audioEncoder_ = std::make_unique(); + videoEncoder_ = std::make_unique(); + muxer_ = std::make_unique(); + + int32_t ret = videoEncoder_->Create(sampleInfo_.outputVideoCodecMime); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed"); + ret = muxer_->Create(sampleInfo_.outputFd); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create muxer with fd(%{public}d) failed", + sampleInfo_.outputFd); + ret = muxer_->Config(sampleInfo_); + +// ret = CreateAudioEncoder(); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create audio encoder failed"); + + ret = CreateVideoEncoder(); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed"); + +// releaseThread_ = nullptr; + AVCODEC_SAMPLE_LOGI("Succeed"); + return AVCODEC_SAMPLE_ERR_OK; +} + + +int32_t Player::Start() { + std::unique_lock lock(mutex_); + int32_t ret; + CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + CHECK_AND_RETURN_RET_LOG(demuxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + if (videoDecContext_) { + ret = videoDecoder_->Start(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Video Decoder start failed"); + lock.unlock(); + StartDecRelease(); + return AVCODEC_SAMPLE_ERR_ERROR; + } + isStarted_ = true; + videoDecInputThread_ = std::make_unique(&Player::VideoDecInputThread, this); + videoDecOutputThread_ = std::make_unique(&Player::VideoDecOutputThread, this); + + if (videoDecInputThread_ == nullptr || videoDecOutputThread_ == nullptr) { + AVCODEC_SAMPLE_LOGE("Create thread failed"); + lock.unlock(); + StartDecRelease(); + return AVCODEC_SAMPLE_ERR_ERROR; + } + } + + if (videoEncContext_) { + CHECK_AND_RETURN_RET_LOG(videoEncoder_ != nullptr && muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, + "Already started."); + int32_t ret = muxer_->Start(); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Muxer start failed"); + ret = videoEncoder_->Start(); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder start failed"); + videoEncOutputThread_ = std::make_unique(&Player::VideoEncOutputThread, this); + if (videoEncOutputThread_ == nullptr) { + AVCODEC_SAMPLE_LOGE("Create thread failed"); + StartEncRelease(); + return AVCODEC_SAMPLE_ERR_ERROR; + } + } + +// if (audioDecContext_) { +// ret = audioDecoder_->Start(); +// if (ret != AVCODEC_SAMPLE_ERR_OK) { +// AVCODEC_SAMPLE_LOGE("Audio Decoder start failed"); +// lock.unlock(); +// StartRelease(); +// return AVCODEC_SAMPLE_ERR_ERROR; +// } +// isStarted_ = true; +// audioDecInputThread_ = std::make_unique(&Player::AudioDecInputThread, this); +// audioDecOutputThread_ = std::make_unique(&Player::AudioDecOutputThread, this); +// if (audioDecInputThread_ == nullptr || audioDecOutputThread_ == nullptr) { +// AVCODEC_SAMPLE_LOGE("Create thread failed"); +// lock.unlock(); +// StartRelease(); +// return AVCODEC_SAMPLE_ERR_ERROR; +// } +// } + // Clear the queue +// while (audioDecContext_ && !audioDecContext_->renderQueue.empty()) { +// audioDecContext_->renderQueue.pop(); +// } + + AVCODEC_SAMPLE_LOGI("Succeed"); + doneCond_.notify_all(); + return AVCODEC_SAMPLE_ERR_OK; +} + +void Player::Stop() { + if (!isReleased_) { + isReleased_ = true; + DecRelease(); + } + StartEncRelease(); +} + +void Player::StartDecRelease() { + AVCODEC_SAMPLE_LOGI("start release"); + std::unique_lock lock(doneMutex); + doneCond_.wait(lock, [this]() { return isAudioDone.load(); }); + + if (!isReleased_) { + isReleased_ = true; + DecRelease(); + } +} + +void Player::ReleaseThread() { + if (videoDecInputThread_ && videoDecInputThread_->joinable()) { + videoDecInputThread_->detach(); + videoDecInputThread_.reset(); + } + if (videoDecOutputThread_ && videoDecOutputThread_->joinable()) { + videoDecOutputThread_->detach(); + videoDecOutputThread_.reset(); + } + if (audioDecInputThread_ && audioDecInputThread_->joinable()) { + audioDecInputThread_->detach(); + audioDecInputThread_.reset(); + } + if (audioDecOutputThread_ && audioDecOutputThread_->joinable()) { + audioDecOutputThread_->detach(); + audioDecOutputThread_.reset(); + } +} + +void Player::DecRelease() { + std::lock_guard lock(mutex_); + ReleaseThread(); + + if (demuxer_ != nullptr) { + demuxer_->Release(); + demuxer_.reset(); + } + if (videoDecoder_ != nullptr) { + videoDecoder_->Release(); + videoDecoder_.reset(); + } + if (videoDecContext_ != nullptr) { + delete videoDecContext_; + videoDecContext_ = nullptr; + } + if (audioDecoder_ != nullptr) { + audioDecoder_->Release(); + audioDecoder_.reset(); + } + if (audioDecContext_ != nullptr) { + delete audioDecContext_; + audioDecContext_ = nullptr; + } + OH_AudioStreamBuilder_Destroy(builder_); + builder_ = nullptr; + doneCond_.notify_all(); + // Trigger the callback + sampleInfo_.playDoneCallback(sampleInfo_.playDoneCallbackData); + // Clear the queue + while (audioDecContext_ && !audioDecContext_->renderQueue.empty()) { + audioDecContext_->renderQueue.pop(); + } + AVCODEC_SAMPLE_LOGI("Succeed"); +} + +void Player::StartEncRelease() { + if (releaseEncThread_ == nullptr) { + AVCODEC_SAMPLE_LOGI("Start release CodecTest"); + releaseEncThread_ = std::make_unique(&Player::EncRelease, this); + } +} + +void Player::EncRelease() { + std::lock_guard lock(mutex_); + isStarted_ = false; + if (videoEncOutputThread_ && videoEncOutputThread_->joinable()) { + videoEncOutputThread_->join(); + videoEncOutputThread_.reset(); + } + if (audioEncInputThread_ && audioEncInputThread_->joinable()) { + audioEncContext_->inputCond.notify_all(); + audioEncInputThread_->join(); + audioEncInputThread_.reset(); + } + if (audioEncOutputThread_ && audioEncOutputThread_->joinable()) { + audioEncContext_->outputCond.notify_all(); + audioEncOutputThread_->join(); + audioEncOutputThread_.reset(); + } + if (muxer_ != nullptr) { + muxer_->Release(); + muxer_.reset(); + AVCODEC_SAMPLE_LOGI("Muxer release successful"); + } + if (videoEncoder_ != nullptr) { + videoEncoder_->Stop(); + if (sampleInfo_.window != nullptr) { + OH_NativeWindow_DestroyNativeWindow(sampleInfo_.window); + sampleInfo_.window = nullptr; + } + videoEncoder_->Release(); + videoEncoder_.reset(); + AVCODEC_SAMPLE_LOGI("Video encoder release successful"); + } + if (audioEncoder_ != nullptr) { + audioEncoder_->Stop(); + audioEncoder_->Release(); + audioEncoder_.reset(); + AVCODEC_SAMPLE_LOGI("Audio encoder release successful"); + } +// if (audioCapturer_ != nullptr) { +// audioCapturer_->AudioCapturerRelease(); +// audioCapturer_.reset(); +// AVCODEC_SAMPLE_LOGI("Audio Capturer release successful"); +// } + if (audioEncContext_ != nullptr) { + delete audioEncContext_; + audioEncContext_ = nullptr; + } + if (videoEncContext_ != nullptr) { + delete videoEncContext_; + videoEncContext_ = nullptr; + } + doneCond_.notify_all(); + AVCODEC_SAMPLE_LOGI("Succeed"); +} + +void Player::CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBufferInfo &bufferInfo) { + auto &info = sampleInfo_; + int32_t videoWidth = + videoDecContext_->width * + ((info.videoCodecMime == OH_AVCODEC_MIMETYPE_VIDEO_HEVC && info.hevcProfile == HEVC_PROFILE_MAIN_10) ? 2 : 1); + int32_t &stride = videoDecContext_->widthStride; + int32_t uvWidth = videoWidth / YUV420_SAMPLE_RATIO; + int32_t uvStride = stride / YUV420_SAMPLE_RATIO; + int32_t size = 0; + uint8_t *tempBufferAddr = encBufferInfo.bufferAddr; + + // copy Y + for (int32_t row = 0; row < videoDecContext_->height; row++) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, videoWidth); + tempBufferAddr += videoWidth; + bufferInfo.bufferAddr += stride; + } + size += videoDecContext_->height * videoWidth; + bufferInfo.bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) * stride; + + // copy U + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); + tempBufferAddr += uvWidth; + bufferInfo.bufferAddr += uvStride; + } + bufferInfo.bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) / YUV420_SAMPLE_RATIO * uvStride; + + // copy V + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); + tempBufferAddr += uvWidth; + bufferInfo.bufferAddr += uvStride; + } + + size += videoDecContext_->height * uvWidth; + encBufferInfo.attr.flags = bufferInfo.attr.flags; + encBufferInfo.attr.offset = bufferInfo.attr.offset; + encBufferInfo.attr.pts = bufferInfo.attr.pts; + encBufferInfo.attr.size = size; + tempBufferAddr = nullptr; + delete tempBufferAddr; +} + +void Player::VideoDecInputThread() { + while (true) { + CHECK_AND_BREAK_LOG(isStarted_, "Decoder input thread out"); + std::unique_lock lock(videoDecContext_->inputMutex); + bool condRet = videoDecContext_->inputCond.wait_for( + lock, 5s, [this]() { return !isStarted_ || !videoDecContext_->inputBufferInfoQueue.empty(); }); + CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out"); + CHECK_AND_CONTINUE_LOG(!videoDecContext_->inputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + + CodecBufferInfo bufferInfo = videoDecContext_->inputBufferInfoQueue.front(); + videoDecContext_->inputBufferInfoQueue.pop(); + videoDecContext_->inputFrameCount++; + lock.unlock(); + + demuxer_->ReadSample(demuxer_->GetVideoTrackId(), reinterpret_cast(bufferInfo.buffer), + bufferInfo.attr); + + int32_t ret = videoDecoder_->PushInputBuffer(bufferInfo); + CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Push data failed, thread out"); + + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "VideoDecInputThread Catch EOS, thread out"); + } +} + +void Player::VideoDecOutputThread() { + isVideoDone = false; + sampleInfo_.frameInterval = MICROSECOND / sampleInfo_.frameRate; + while (true) { + thread_local auto lastPushTime = std::chrono::system_clock::now(); + CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); + std::unique_lock lock(videoDecContext_->outputMutex); + bool condRet = videoDecContext_->outputCond.wait_for(lock, 5s, [this]() { + return !isStarted_ || + !(videoDecContext_->outputBufferInfoQueue.empty() && videoEncContext_->inputBufferInfoQueue.empty()); + }); + CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); + CHECK_AND_CONTINUE_LOG(!videoDecContext_->outputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + CHECK_AND_CONTINUE_LOG(!videoEncContext_->inputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + + CodecBufferInfo bufferInfo = videoDecContext_->outputBufferInfoQueue.front(); + videoDecContext_->outputBufferInfoQueue.pop(); + videoDecContext_->outputFrameCount++; + AVCODEC_SAMPLE_LOGW("Out buffer count: %{public}u, size: %{public}d, flag: %{public}u, pts: %{public}" PRId64, + videoDecContext_->outputFrameCount, bufferInfo.attr.size, bufferInfo.attr.flags, + bufferInfo.attr.pts); + lock.unlock(); + + // 将数据写入到编码队列 + CodecBufferInfo encBufferInfo = videoEncContext_->inputBufferInfoQueue.front(); + videoEncContext_->inputBufferInfoQueue.pop(); + videoEncContext_->inputFrameCount++; + + + AVCODEC_SAMPLE_LOGW("Out bufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}" PRId64, + bufferInfo.attr.flags, bufferInfo.attr.offset, bufferInfo.attr.pts, + bufferInfo.attr.size); + + + encBufferInfo.bufferAddr = OH_AVBuffer_GetAddr(reinterpret_cast(encBufferInfo.buffer)); + bufferInfo.bufferAddr = OH_AVBuffer_GetAddr(reinterpret_cast(bufferInfo.buffer)); + CopyStrideYUV420SP(encBufferInfo, bufferInfo); + + AVCODEC_SAMPLE_LOGW("Out encBufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}d" PRId64, + encBufferInfo.attr.flags, encBufferInfo.attr.offset, encBufferInfo.attr.pts, + encBufferInfo.attr.size); + + OH_AVBuffer_SetBufferAttr(reinterpret_cast(encBufferInfo.buffer), &encBufferInfo.attr); + + // 释放解码输出队列资源 + int32_t ret = videoDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, false); + CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Decoder output thread out"); + + // 将缓存推送给编码器 + videoEncoder_->PushInputBuffer(encBufferInfo); + + if (bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS) { + AVCODEC_SAMPLE_LOGW("VideoDecOutputThread Catch EOS, thread out" PRId64); + break; + } + + std::this_thread::sleep_until(lastPushTime + std::chrono::microseconds(sampleInfo_.frameInterval)); + lastPushTime = std::chrono::system_clock::now(); + } + isVideoDone = true; + StartDecRelease(); +} + +void Player::VideoEncOutputThread() { + while (true) { + std::unique_lock lock(videoEncContext_->outputMutex); + bool condRet = videoEncContext_->outputCond.wait_for( + lock, 5s, [this]() { return !isStarted_ || !videoEncContext_->outputBufferInfoQueue.empty(); }); + CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out"); + CHECK_AND_CONTINUE_LOG(!videoEncContext_->outputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + + CodecBufferInfo bufferInfo = videoEncContext_->outputBufferInfoQueue.front(); + videoEncContext_->outputBufferInfoQueue.pop(); + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "VideoEncOutputThread Catch EOS, thread out"); + lock.unlock(); + if ((bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_SYNC_FRAME) || + (bufferInfo.attr.flags == AVCODEC_BUFFER_FLAGS_NONE)) { + videoEncContext_->outputFrameCount++; + bufferInfo.attr.pts = videoEncContext_->outputFrameCount * MICROSECOND / sampleInfo_.frameRate; + } else { + bufferInfo.attr.pts = 0; + } + AVCODEC_SAMPLE_LOGW("Out buffer count: %{public}u, size: %{public}d, flag: %{public}u, pts: %{public}" PRId64, + videoEncContext_->outputFrameCount, bufferInfo.attr.size, bufferInfo.attr.flags, + bufferInfo.attr.pts); + + muxer_->WriteSample(muxer_->GetVideoTrackId(), reinterpret_cast(bufferInfo.buffer), + bufferInfo.attr); + int32_t ret = videoEncoder_->FreeOutputBuffer(bufferInfo.bufferIndex); + CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Encoder output thread out"); + } + AVCODEC_SAMPLE_LOGI("Exit, frame count: %{public}u", videoEncContext_->outputFrameCount); + StartEncRelease(); +} + + +void Player::AudioDecInputThread() { + while (true) { + CHECK_AND_BREAK_LOG(isStarted_, "Decoder input thread out"); + std::unique_lock lock(audioDecContext_->inputMutex); + bool condRet = audioDecContext_->inputCond.wait_for( + lock, 5s, [this]() { return !isStarted_ || !audioDecContext_->inputBufferInfoQueue.empty(); }); + CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out"); + CHECK_AND_CONTINUE_LOG(!audioDecContext_->inputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + + CodecBufferInfo bufferInfo = audioDecContext_->inputBufferInfoQueue.front(); + audioDecContext_->inputBufferInfoQueue.pop(); + audioDecContext_->inputFrameCount++; + lock.unlock(); + + demuxer_->ReadSample(demuxer_->GetAudioTrackId(), reinterpret_cast(bufferInfo.buffer), + bufferInfo.attr); + + int32_t ret = audioDecoder_->PushInputBuffer(bufferInfo); + CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Push data failed, thread out"); + + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out"); + } +} + +void Player::AudioDecOutputThread() { + isAudioDone = false; + while (true) { + CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); + std::unique_lock lock(audioDecContext_->outputMutex); + bool condRet = audioDecContext_->outputCond.wait_for( + lock, 5s, [this]() { return !isStarted_ || !audioDecContext_->outputBufferInfoQueue.empty(); }); + CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); + CHECK_AND_CONTINUE_LOG(!audioDecContext_->outputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + + CodecBufferInfo bufferInfo = audioDecContext_->outputBufferInfoQueue.front(); + audioDecContext_->outputBufferInfoQueue.pop(); + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out"); + audioDecContext_->outputFrameCount++; + AVCODEC_SAMPLE_LOGW("Out buffer count: %{public}u, size: %{public}d, flag: %{public}u, pts: %{public}" PRId64, + audioDecContext_->outputFrameCount, bufferInfo.attr.size, bufferInfo.attr.flags, + bufferInfo.attr.pts); + uint8_t *source = OH_AVBuffer_GetAddr(reinterpret_cast(bufferInfo.buffer)); + // Put the decoded PMC data into the queue + for (int i = 0; i < bufferInfo.attr.size; i++) { + audioDecContext_->renderQueue.push(*(source + i)); + } + lock.unlock(); + + int32_t ret = audioDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, true); + CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Decoder output thread out"); + + std::unique_lock lockRender(audioDecContext_->renderMutex); + audioDecContext_->renderCond.wait_for(lockRender, 20ms, [this, bufferInfo]() { + return audioDecContext_->renderQueue.size() < BALANCE_VALUE * bufferInfo.attr.size; + }); + } + std::unique_lock lockRender(audioDecContext_->renderMutex); + audioDecContext_->renderCond.wait_for(lockRender, 500ms, + [this]() { return audioDecContext_->renderQueue.size() < 1; }); + isAudioDone = true; + AVCODEC_SAMPLE_LOGI("Out buffer end"); + StartDecRelease(); +} \ No newline at end of file diff --git a/entry/src/main/cpp/sample/player/Player.h b/entry/src/main/cpp/sample/player/Player.h new file mode 100644 index 0000000..6bf38d2 --- /dev/null +++ b/entry/src/main/cpp/sample/player/Player.h @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef VIDEO_CODEC_PLAYER_H +#define VIDEO_CODEC_PLAYER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "VideoDecoder.h" +#include "AudioDecoder.h" +#include "multimedia/player_framework/native_avbuffer.h" +#include "Demuxer.h" +#include "SampleInfo.h" +#include "VideoEncoder.h" +#include "AudioEncoder.h" +#include "Muxer.h" + +class Player { +public: + Player(){}; + ~Player(); + + static Player &GetInstance() { + static Player player; + return player; + } + + int32_t Init(SampleInfo &sampleInfo); + int32_t InitDecoder(); + int32_t InitEncoder(); + int32_t Start(); + void Stop(); + +private: + void VideoDecInputThread(); + void VideoDecOutputThread(); + void VideoEncOutputThread(); + void AudioDecInputThread(); + void AudioDecOutputThread(); + void DecRelease(); + void CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBufferInfo &bufferInfo); + void StartDecRelease(); + void EncRelease(); + void StartEncRelease(); + void ReleaseThread(); + int32_t CreateAudioDecoder(); + int32_t CreateVideoDecoder(); + int32_t CreateAudioEncoder(); + int32_t CreateVideoEncoder(); + + std::unique_ptr videoDecoder_ = nullptr; + std::shared_ptr audioDecoder_ = nullptr; + std::unique_ptr demuxer_ = nullptr; + + std::unique_ptr videoEncoder_ = nullptr; + std::unique_ptr audioEncoder_ = nullptr; + std::unique_ptr muxer_ = nullptr; + + std::mutex mutex_; + std::mutex doneMutex; + std::atomic isStarted_{false}; + std::atomic isReleased_{false}; + std::atomic isVideoDone{true}; + std::atomic isAudioDone{true}; + std::unique_ptr videoDecInputThread_ = nullptr; + std::unique_ptr videoDecOutputThread_ = nullptr; + std::unique_ptr audioDecInputThread_ = nullptr; + std::unique_ptr audioDecOutputThread_ = nullptr; + + std::unique_ptr videoEncOutputThread_ = nullptr; + std::unique_ptr audioEncInputThread_ = nullptr; + std::unique_ptr audioEncOutputThread_ = nullptr; + std::unique_ptr releaseEncThread_ = nullptr; + std::condition_variable doneCond_; + SampleInfo sampleInfo_; + CodecUserData *videoDecContext_ = nullptr; + CodecUserData *audioDecContext_ = nullptr; + CodecUserData *videoEncContext_ = nullptr; + CodecUserData *audioEncContext_ = nullptr; + + std::mutex yuvMutex; + std::condition_variable yuvCond; + std::queue yuvBufferQueue; + + OH_AudioStreamBuilder *builder_ = nullptr; + OH_AudioRenderer *audioRenderer_ = nullptr; + static constexpr int64_t MICROSECOND = 1000000; +}; + +#endif // VIDEO_CODEC_PLAYER_H \ No newline at end of file diff --git a/entry/src/main/cpp/sample/player/PlayerNative.cpp b/entry/src/main/cpp/sample/player/PlayerNative.cpp new file mode 100644 index 0000000..ab6448f --- /dev/null +++ b/entry/src/main/cpp/sample/player/PlayerNative.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PlayerNative.h" + +#undef LOG_DOMAIN +#undef LOG_TAG +#define LOG_DOMAIN 0xFF00 +#define LOG_TAG "player" + +struct CallbackContext { + napi_env env = nullptr; + napi_ref callbackRef = nullptr; +}; + +void Callback(void *asyncContext) { + uv_loop_s *loop = nullptr; + CallbackContext *context = (CallbackContext *)asyncContext; + napi_get_uv_event_loop(context->env, &loop); + uv_work_t *work = new uv_work_t; + work->data = context; + uv_queue_work( + loop, work, [](uv_work_t *work) {}, + [](uv_work_t *work, int status) { + CallbackContext *context = (CallbackContext *)work->data; + napi_handle_scope scope = nullptr; + // Manage the lifecycle of napi_value to prevent memory leaks. + napi_open_handle_scope(context->env, &scope); + napi_value callback = nullptr; + napi_get_reference_value(context->env, context->callbackRef, &callback); + // Callback to UI side. + napi_call_function(context->env, nullptr, callback, 0, nullptr, nullptr); + napi_close_handle_scope(context->env, scope); + delete context; + delete work; + }); +} + +napi_value PlayerNative::Play(napi_env env, napi_callback_info info) { + SampleInfo sampleInfo; + size_t argc = 10; + napi_value args[10] = {nullptr}; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + napi_get_value_int32(env, args[0], &sampleInfo.inputFd); + napi_get_value_int32(env, args[1], &sampleInfo.outputFd); + napi_get_value_int64(env, args[2], &sampleInfo.inputFileOffset); + napi_get_value_int64(env, args[3], &sampleInfo.inputFileSize); + char videoCodecMime[20] = {0}; + size_t videoCodecMimeStrlen = 0; + size_t len = 20; + napi_get_value_string_utf8(env, args[4], videoCodecMime, len, &videoCodecMimeStrlen); + sampleInfo.outputVideoCodecMime = videoCodecMime; + napi_get_value_int32(env, args[5], &sampleInfo.outputVideoWidth); + napi_get_value_int32(env, args[6], &sampleInfo.outputVideoHeight); + napi_get_value_double(env, args[7], &sampleInfo.outputFrameRate); + napi_get_value_int64(env, args[8], &sampleInfo.outputBitrate); + + auto asyncContext = new CallbackContext(); + asyncContext->env = env; + napi_create_reference(env, args[9], 1, &asyncContext->callbackRef); + + sampleInfo.playDoneCallback = &Callback; + sampleInfo.playDoneCallbackData = asyncContext; + int32_t ret = Player::GetInstance().Init(sampleInfo); + if (ret == AVCODEC_SAMPLE_ERR_OK) { + Player::GetInstance().Start(); + } + return nullptr; +} + +napi_value PlayerNative::Stop(napi_env env, napi_callback_info info) { + Player::GetInstance().Stop(); + return nullptr; +} + +EXTERN_C_START +static napi_value Init(napi_env env, napi_value exports) { + napi_property_descriptor classProp[] = { + {"playNative", nullptr, PlayerNative::Play, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"stopNative", nullptr, PlayerNative::Stop, nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + + napi_define_properties(env, exports, sizeof(classProp) / sizeof(classProp[0]), classProp); + return exports; +} +EXTERN_C_END + +static napi_module PlayerModule = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = nullptr, + .nm_register_func = Init, + .nm_modname = "player", + .nm_priv = ((void *)0), + .reserved = {0}, +}; + +extern "C" __attribute__((constructor)) void RegisterPlayerModule(void) { napi_module_register(&PlayerModule); } \ No newline at end of file diff --git a/entry/src/main/cpp/sample/player/PlayerNative.h b/entry/src/main/cpp/sample/player/PlayerNative.h new file mode 100644 index 0000000..cb602d2 --- /dev/null +++ b/entry/src/main/cpp/sample/player/PlayerNative.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef VIDEO_CODEC_SAMPLE_PLAYER_NATIVE_H +#define VIDEO_CODEC_SAMPLE_PLAYER_NATIVE_H + +#include +#include +#include +#include +#include "napi/native_api.h" +#include "Player.h" +#include "dfx/error/AVCodecSampleError.h" +#include "AVCodecSampleLog.h" + +class PlayerNative { +public: + static napi_value Play(napi_env env, napi_callback_info info); + static napi_value Stop(napi_env env, napi_callback_info info); +}; +#endif // VIDEO_CODEC_SAMPLE_PLAYER_NATIVE_H \ No newline at end of file diff --git a/entry/src/main/cpp/types/libplayer/index.d.ts b/entry/src/main/cpp/types/libplayer/index.d.ts new file mode 100644 index 0000000..dda6388 --- /dev/null +++ b/entry/src/main/cpp/types/libplayer/index.d.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const playNative: ( + inputFileFd: number, + outputFileFd: number, + inputFileOffset: number, + inputFileSize: number, + videoCodecMime: string, + width: number, + height: number, + frameRate: number, + bitRate: number, + cbFn: () => void +) => void; + +export const stopNative: () => void; \ No newline at end of file diff --git a/entry/src/main/cpp/types/libplayer/oh-package.json5 b/entry/src/main/cpp/types/libplayer/oh-package.json5 new file mode 100644 index 0000000..d8dedda --- /dev/null +++ b/entry/src/main/cpp/types/libplayer/oh-package.json5 @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "name": "libplayer.so", + "types": "./index.d.ts", + "version": "1.0.0", + "decription": "Player interface." +} \ No newline at end of file diff --git a/entry/src/main/ets/common/CommonConstants.ets b/entry/src/main/ets/common/CommonConstants.ets new file mode 100644 index 0000000..d6a6601 --- /dev/null +++ b/entry/src/main/ets/common/CommonConstants.ets @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { camera } from '@kit.CameraKit'; + +export class CommonConstants { + /** + * Index page Tag. + */ + static readonly INDEX_TAG: string = 'INDEX'; + /** + * Recorder page Tag. + */ + static readonly RECORDER_TAG: string = 'RECORDER'; + /** + * Default ID. + */ + static readonly DEFAULT_ID: string = '-1'; + /** + * Default time. + */ + static readonly DEFAULT_TIME: string = '00:00'; + /** + * PX. + */ + static readonly PX: string = 'px'; + /** + * Default value. + */ + static readonly DEFAULT_VALUE: number = 0; + /** + * Default profile. + */ + static readonly DEFAULT_PROFILE: camera.Profile = { + format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP, + size: { + width: 1920, + height: 1080 + } + }; + /** + * Video avc mime type. + */ + static readonly MIME_VIDEO_AVC: string = 'video/avc'; + /** + * Video hevc mime type. + */ + static readonly MIME_VIDEO_HEVC: string = 'video/hevc'; + /** + * Default width. + */ + static readonly DEFAULT_WIDTH: number = 1920; + /** + * Default height. + */ + static readonly DEFAULT_HEIGHT: number = 1080; + /** + * 4K video width. + */ + static readonly VIDEO_WIDTH_4K: number = 3840; + /** + * 4K video height. + */ + static readonly VIDEO_HEIGHT_4K: number = 2160; + /** + * 1080P video width. + */ + static readonly VIDEO_WIDTH_1080P: number = 1920; + /** + * 1080P video height. + */ + static readonly VIDEO_HEIGHT_1080P: number = 1080; + /** + * 720P video width. + */ + static readonly VIDEO_WIDTH_720P: number = 1280; + /** + * 720P video height. + */ + static readonly VIDEO_HEIGHT_720P: number = 720; + /** + * 10M bitrate. + */ + static readonly BITRATE_VIDEO_10M: number = 10 * 1024 * 1024; + /** + * 20M bitrate. + */ + static readonly BITRATE_VIDEO_20M: number = 20 * 1024 * 1024; + /** + * 30M bitrate. + */ + static readonly BITRATE_VIDEO_30M: number = 30 * 1024 * 1024; + /** + * 30 FPS. + */ + static readonly FRAMERATE_VIDEO_30FPS: number = 30; + /** + * 60 FPS. + */ + static readonly FRAMERATE_VIDEO_60FPS: number = 60; + /** + * Duration. + */ + static readonly DURATION: number = 2000; + /** + * The distance between toast dialog box and the bottom of screen. + */ + static readonly BOTTOM: number = 80; + /** + * Default picker item height. + */ + static readonly DEFAULT_PICKER_ITEM_HEIGHT: number = 30; + /** + * Selected text style font size. + */ + static readonly SELECTED_TEXT_STYLE_FONT_SIZE: number = 15; + /** + * Video mime type. + */ + static readonly VIDEO_MIMETYPE: string[] = ['H264', 'H265']; + /** + * Video resolution. + */ + static readonly VIDEO_RESOLUTION: string[] = ['4K', '1080P', '720P']; + /** + * Video framerate. + */ + static readonly VIDEO_FRAMERATE: string[] = ['30Fps', '60Fps']; + /** + * Video recorderInfo. + */ + static readonly RECORDER_INFO: string[][] = [ + CommonConstants.VIDEO_MIMETYPE, CommonConstants.VIDEO_RESOLUTION, CommonConstants.VIDEO_FRAMERATE + ]; + /** + * The number corresponding to true. + */ + static readonly TRUE: number = 1; + /** + * The number corresponding to false. + */ + static readonly FALSE: number = 0; + /** + * Min range. + */ + static readonly MIN_RANGE: number = 1; + /** + * Max range. + */ + static readonly MAX_RANGE: number = 30; + /** + * Full size. + */ + static readonly FULL_SIZE: string = '100%'; +} \ No newline at end of file diff --git a/entry/src/main/ets/common/utils/Logger.ets b/entry/src/main/ets/common/utils/Logger.ets new file mode 100644 index 0000000..82525da --- /dev/null +++ b/entry/src/main/ets/common/utils/Logger.ets @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; + +class Logger { + private domain: number; + private format: string = '%{public}s'; + + constructor() { + this.domain = 0xFF00; + } + + debug(...arg: string[]): void { + hilog.debug(this.domain, arg[0], this.format, arg[1]); + } + + info(...arg: string[]): void { + hilog.info(this.domain, arg[0], this.format, arg[1]); + } + + warn(...arg: string[]): void { + hilog.warn(this.domain, arg[0], this.format, arg[1]); + } + + error(...arg: string[]): void { + hilog.error(this.domain, arg[0], this.format, arg[1]); + } +} + +export default new Logger(); \ No newline at end of file diff --git a/entry/src/main/ets/common/utils/TimeUtils.ets b/entry/src/main/ets/common/utils/TimeUtils.ets new file mode 100644 index 0000000..16e0cf7 --- /dev/null +++ b/entry/src/main/ets/common/utils/TimeUtils.ets @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Time handling function. + * @param time + * @returns + */ +export function formatVideoTime(totalSeconds: number): string { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; +} \ No newline at end of file diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..a9c6343 --- /dev/null +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { abilityAccessCtrl, AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import Logger from '../common/utils/Logger'; + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + windowStage.getMainWindowSync().setWindowKeepScreenOn(true); + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + AppStorage.setOrCreate("context", windowStage.getMainWindowSync().getUIContext()); + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); + } +}; \ No newline at end of file diff --git a/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000..7b815b4 --- /dev/null +++ b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(0x0000, 'testTag', 'onBackup ok'); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(0x0000, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/model/VideoDataModel.ets b/entry/src/main/ets/model/VideoDataModel.ets new file mode 100644 index 0000000..d613862 --- /dev/null +++ b/entry/src/main/ets/model/VideoDataModel.ets @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { camera } from '@kit.CameraKit'; +import { CommonConstants as Const } from '../common/CommonConstants'; + +export class VideoDataModel { + public surfaceId: string = ''; + public width: number = Const.DEFAULT_WIDTH; + public height: number = Const.DEFAULT_HEIGHT; + public isHDRVivid: number = Const.DEFAULT_VALUE; + public outputfd: number = -1; + public frameRate: number = Const.FRAMERATE_VIDEO_30FPS; + public previewProfile: camera.Profile = Const.DEFAULT_PROFILE; + public videoCodecMime: string | null = Const.MIME_VIDEO_AVC; + public bitRate: number = Const.BITRATE_VIDEO_20M; + + setCodecFormat(isHDR: number, codecMime: string): void { + this.isHDRVivid = isHDR; + this.videoCodecMime = codecMime; + } + + setResolution(width: number, height: number, bit: number): void { + this.width = width; + this.height = height; + this.bitRate = bit; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..1d9fb3b --- /dev/null +++ b/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import Logger from '../common/utils/Logger'; +import { CommonConstants as Const } from '../common/CommonConstants'; +import { VideoTranscoding } from './VideoTranscoding'; +import { VideoPlayer } from './VideoPlayer'; + +const TAG: string = Const.INDEX_TAG; + +@Entry +@Component +struct Index { + @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack(); + @State selectFilePath: string | null = null; + + @Builder + PagesMap(name: string) { + if (name === 'VideoTranscoding') { + NavDestination() { + VideoTranscoding() + } + .title('视频转码') + .backgroundColor('#F1F3F5') + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + } else if (name === 'VideoPlayer') { + NavDestination() { + VideoPlayer() + } + .title('视频播放') + .backgroundColor('#F1F3F5') + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + } + } + + build() { + Navigation(this.pageStack) { + Column() { + Text('基于Buffer模式进行转码') + .width('100%') + .fontColor('#E6000000') + .fontSize(30) + .fontWeight(700) + .lineHeight(40) + .textAlign(TextAlign.Start) + .margin({ top: 64 }) + + Blank() + + Button($r('app.string.select_video')) + .onClick(() => { + this.selectFile(); + }) + .size({ + width: '100%', + height: 40 + }) + .margin({ bottom: 12 }) + } + .padding({ + left: 16, + right: 16, + bottom: 16 + }) + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Start) + } + .hideTitleBar(true) + .mode(NavigationMode.Stack) + .backgroundColor('#F1F3F5') + .navDestination(this.PagesMap) + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) + } + + selectFile() { + let photoPicker = new photoAccessHelper.PhotoViewPicker(); + photoPicker.select({ + MIMEType: photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE, + maxSelectNumber: 1 + }) + .then((photoSelectResult) => { + this.selectFilePath = photoSelectResult.photoUris[0]; + if (this.selectFilePath === null) { + this.getUIContext().getPromptAction().showToast({ + message: $r('app.string.alert'), + duration: Const.DURATION, + bottom: Const.BOTTOM + }); + } else { + AppStorage.setOrCreate('selectedFile', this.selectFilePath); + this.pageStack.pushPathByName('VideoTranscoding',''); + Logger.info(TAG, 'documentViewPicker.select to file succeed and URI is:' + this.selectFilePath); + } + }); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/VideoPlayer.ets b/entry/src/main/ets/pages/VideoPlayer.ets new file mode 100644 index 0000000..e0a2327 --- /dev/null +++ b/entry/src/main/ets/pages/VideoPlayer.ets @@ -0,0 +1,239 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { formatVideoTime } from '../common/utils/TimeUtils'; + +@Component +export struct VideoPlayer { + @Consume('NavPathStack') pageInfos: NavPathStack; + @State currentTime: number = 0 + @State durationTime: number = 0 + @State isStart: boolean = true; + @State transCurrentTime: number = 0 + @State transDurationTime: number = 0 + @State transIsStart: boolean = true; + @State currentIndex: number = 0; + @State tabColorOne: string = '#191919'; + @State tabColorTwo: string = '#666666'; + @State backgroundColorOne: string = '#FFFFFF'; + @State backgroundColorTwo: string = '#E5E5E5'; + private tabsController: TabsController = new TabsController(); + private controller: VideoController = new VideoController(); + private transController: VideoController = new VideoController(); + + build() { + Row() { + Column() { + // Video area + Row() { + Button('转码前') + .onClick(() => { + this.tabsController.changeIndex(0); + this.currentIndex = 0; + this.changeIndex(); + }) + .fontColor(this.tabColorOne) + .backgroundColor(this.backgroundColorOne) + .margin(2) + .width(80) + Button('转码后') + .onClick(() => { + this.tabsController.changeIndex(1); + this.currentIndex = 1; + this.changeIndex(); + }) + .fontColor(this.tabColorTwo) + .backgroundColor(this.backgroundColorTwo) + .margin(2) + .width(80) + } + .borderRadius(20) + .backgroundColor('#E5E5E5') + .margin({ bottom: 16 }) + .width(168) + + Tabs({ index: this.currentIndex, controller: this.tabsController }) { + TabContent() { + Column() { + Stack() { + Video({ src: AppStorage.get('selectedFile'), controller: this.controller }) + .width('100%') + .height('100%') + .autoPlay(true) + .controls(false) + .objectFit(1) + .zIndex(0) + .onPrepared((event) => { + if (event) { + this.durationTime = event.duration + console.info(this.durationTime.toString()); + } + }) + .onUpdate((event) => { + if (event) { + this.currentTime = event.time + } + }) + .onFinish(() => { + this.isStart = !this.isStart; + }) + .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) + + Row() { + Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) + .width(18) + .height(18) + .onClick(() => { + if (this.isStart) { + this.controller.pause(); + this.isStart = !this.isStart; + } else { + this.controller.start(); + this.isStart = !this.isStart; + } + }) + + Text(formatVideoTime(this.currentTime)) + .fontColor(Color.White) + .fontSize(12) + .margin({ left: '12vp' }) + Slider({ + value: this.currentTime, + min: 0, + max: this.durationTime + }) + .onChange((value: number, mode: SliderChangeMode) => { + // Set the video playback progress to jump to the value + this.controller.setCurrentTime(value); + }) + .layoutWeight(1) + Text(formatVideoTime(this.durationTime)) + .fontColor(Color.White) + .fontSize(12) + } + .padding({ left: '16vp', right: '16vp' }) + .zIndex(1) + .height(43) + .width('100%') + } + .alignContent(Alignment.Bottom) + .width('100%') + .layoutWeight(1) + } + .width('100%') + .layoutWeight(1) + } + + TabContent() { + Column() { + Stack() { + Video({ src: 'file://' + AppStorage.get('transcodingFile'), controller: this.transController }) + .width('100%') + .height('100%') + .autoPlay(true) + .controls(false) + .objectFit(1) + .zIndex(0) + .onPrepared((event) => { + if (event) { + this.transDurationTime = event.duration + console.info(this.transDurationTime.toString()); + } + }) + .onUpdate((event) => { + if (event) { + this.transCurrentTime = event.time + } + }) + .onFinish(() => { + this.transIsStart = !this.transIsStart; + }) + .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) + + Row() { + Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) + .width(18) + .height(18) + .onClick(() => { + if (this.transIsStart) { + this.transController.pause(); + this.transIsStart = !this.transIsStart; + } else { + this.transController.start(); + this.transIsStart = !this.transIsStart; + } + }) + + Text(formatVideoTime(this.transCurrentTime)) + .fontColor(Color.White) + .fontSize(12) + .margin({ left: '12vp' }) + Slider({ + value: this.transCurrentTime, + min: 0, + max: this.transDurationTime + }) + .onChange((value: number, mode: SliderChangeMode) => { + // Set the video playback progress to jump to the value + this.transController.setCurrentTime(value); + }) + .layoutWeight(1) + Text(formatVideoTime(this.transDurationTime)) + .fontColor(Color.White) + .fontSize(12) + } + .padding({ left: '16vp', right: '16vp' }) + .zIndex(1) + .height(43) + .width('100%') + } + .alignContent(Alignment.Bottom) + .width('100%') + .layoutWeight(1) + } + .width('100%') + .layoutWeight(1) + } + } + .barHeight(0) + .scrollable(false) + .barMode(BarMode.Fixed) + .height('80%') + + Blank() + + Column() { + Button('返回首页') + .width('100%') + .onClick(() => { + this.pageInfos.popToIndex(-1); + }) + } + .margin({ top: '12vp' }) + .justifyContent(FlexAlign.End) + .padding({ left: 16, right: 16, bottom: 16 }) + .width('100%') + } + .width('100%') + } + .height('100%') + } + + changeIndex() { + this.tabColorOne = this.currentIndex === 0 ? '#191919' : '#666666'; + this.backgroundColorOne = this.currentIndex === 0 ? '#FFFFFF' : '#E5E5E5'; + this.tabColorTwo = this.currentIndex === 1 ? '#191919' : '#666666'; + this.backgroundColorTwo = this.currentIndex === 1 ? '#FFFFFF' : '#E5E5E5'; + } +} diff --git a/entry/src/main/ets/pages/VideoTranscoding.ets b/entry/src/main/ets/pages/VideoTranscoding.ets new file mode 100644 index 0000000..3f54547 --- /dev/null +++ b/entry/src/main/ets/pages/VideoTranscoding.ets @@ -0,0 +1,251 @@ +import { formatVideoTime } from '../common/utils/TimeUtils'; +import { VideoDataModel } from '../model/VideoDataModel'; +import { CommonConstants as Const } from '../common/CommonConstants'; +import player from 'libplayer.so'; +import Logger from '../common/utils/Logger'; +import { fileIo } from '@kit.CoreFileKit'; +import { ImmersiveMode, LevelMode } from '@kit.ArkUI'; + +const TAG: string = Const.INDEX_TAG; + +@Component +export struct VideoTranscoding { + @Consume('NavPathStack') pageStack: NavPathStack; + @State currentTime: number = 0; + @State durationTime: number = 0; + @State isStart: boolean = true; + @State selectedFilesDir: string = ''; + @State isTranscoding: boolean = false; + isConfig: boolean = false; + private videoDataModel: VideoDataModel = new VideoDataModel(); + private controller: VideoController = new VideoController(); + customDialogId: number = 0; + + @Builder + customDialogComponent() { + Column() { + Text('视频转码中') + .fontSize(20) + .height(80) + Progress({ value: 0, total: 100, type: ProgressType.Ring }) + .margin({ top: 16, bottom: 16 }) + .width(120) + .color(Color.Blue) + .style({ strokeWidth: 20, status: ProgressStatus.LOADING }) + Blank().width(50) + Button("关闭") + .width('100%') + .margin(8) + .onClick(() => { + this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogId); + player.stopNative(); + }) + }.padding(20) + } + + build() { + Column() { + Column() { + Stack() { + Video({ src: this.selectedFilesDir, controller: this.controller }) + .width('100%') + .height('100%') + .autoPlay(true) + .controls(false) + .objectFit(1) + .zIndex(0) + .onAppear(() => { + this.selectedFilesDir = AppStorage.get('selectedFile') as string; + console.log('selectedFilesDir:' + this.selectedFilesDir); + }) + .onPrepared((event) => { + if (event) { + this.durationTime = event.duration + console.info(this.durationTime.toString()); + } + }) + .onUpdate((event) => { + if (event) { + this.currentTime = event.time + } + }) + .onFinish(() => { + this.isStart = !this.isStart; + }) + .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) + + Row() { + Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) + .width(18) + .height(18) + .onClick(() => { + if (this.isStart) { + this.controller.pause(); + this.isStart = !this.isStart; + } else { + this.controller.start(); + this.isStart = !this.isStart; + } + }) + + Text(formatVideoTime(this.currentTime)) + .fontColor(Color.White) + .fontSize(12) + .margin({ left: '12vp' }) + Slider({ + value: this.currentTime, + min: 0, + max: this.durationTime + }) + .onChange((value: number, mode: SliderChangeMode) => { + // Set the video playback progress to jump to the value + this.controller.setCurrentTime(value); + }) + .layoutWeight(1) + Text(formatVideoTime(this.durationTime)) + .fontColor(Color.White) + .fontSize(12) + } + .padding({ left: '16vp', right: '16vp' }) + .zIndex(1) + .height(43) + .width('100%') + } + .alignContent(Alignment.Bottom) + .width('100%') + .layoutWeight(1) + } + .backgroundColor('') + .width('100%') + .layoutWeight(1) + + Blank() + + Button($r('app.string.config_video')) + .id('config') + .onClick(() => { + this.getUIContext().showTextPickerDialog({ + defaultPickerItemHeight: Const.DEFAULT_PICKER_ITEM_HEIGHT, + selectedTextStyle: ({ + font: ({ + size: Const.SELECTED_TEXT_STYLE_FONT_SIZE + }) + }), + range: Const.RECORDER_INFO, + canLoop: false, + alignment: DialogAlignment.Center, + backgroundColor: Color.White, + backgroundBlurStyle: BlurStyle.BACKGROUND_ULTRA_THICK, + onAccept: (value: TextPickerResult) => { + switch (value.value[0]) { + case Const.VIDEO_MIMETYPE[0]: { + this.videoDataModel.setCodecFormat(Const.TRUE, Const.MIME_VIDEO_AVC); + break; + } + case Const.VIDEO_MIMETYPE[1]: { + this.videoDataModel.setCodecFormat(Const.FALSE, Const.MIME_VIDEO_HEVC); + break; + } + default: + break; + } + + switch (value.value[1]) { + case Const.VIDEO_RESOLUTION[0]: { + this.videoDataModel.setResolution(Const.VIDEO_WIDTH_4K, Const.VIDEO_HEIGHT_4K, + Const.BITRATE_VIDEO_30M); + break; + } + case Const.VIDEO_RESOLUTION[1]: { + this.videoDataModel.setResolution(Const.VIDEO_WIDTH_1080P, Const.VIDEO_HEIGHT_1080P, + Const.BITRATE_VIDEO_20M); + break; + } + case Const.VIDEO_RESOLUTION[2]: { + this.videoDataModel.setResolution(Const.VIDEO_WIDTH_720P, Const.VIDEO_HEIGHT_720P, + Const.BITRATE_VIDEO_10M); + break; + } + default: + break; + } + + switch (value.value[2]) { + case Const.VIDEO_FRAMERATE[0]: { + this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_30FPS; + break; + } + case Const.VIDEO_FRAMERATE[1]: { + this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_60FPS; + break; + } + default: + break; + } + } + }); + + + }) + .size({ + width: '100%', + height: 40 + }) + .margin({ top: 12, bottom: 12 }) + + Button('开始转码') + .onClick(() => { + const node: FrameNode | null = this.getUIContext().getFrameNodeById("test_text") || null; + this.getUIContext().getPromptAction().openCustomDialog({ + builder: () => { + this.customDialogComponent(); + }, + levelMode: LevelMode.EMBEDDED, // 启用页面级弹出框 + levelUniqueId: node?.getUniqueId(), // 设置页面级弹出框所在页面的任意节点ID + immersiveMode: ImmersiveMode.EXTEND, // 设置页面级弹出框蒙层的显示模式 + }).then((dialogId: number) => { + this.customDialogId = dialogId; + }) + this.transcoding(); + }) + .size({ + width: '100%', + height: $r('app.float.index_button_height') + }) + .margin({ bottom: 12 }) + } + .padding({ + left: 16, + right: 16, + bottom: 16 + }) + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Start) + } + + transcoding() { + let inputFile = fileIo.openSync(this.selectedFilesDir, fileIo.OpenMode.READ_ONLY); + AppStorage.setOrCreate('transcodingFile', this.getUIContext().getHostContext()?.cacheDir + '/transcoding.mp4'); + let outputFile = fileIo.openSync(this.getUIContext().getHostContext()?.cacheDir + '/transcoding.mp4', + fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + if (!inputFile) { + Logger.error(TAG, 'player inputFile is null'); + } + let inputFileState = fileIo.statSync(inputFile.fd); + if (inputFileState.size <= 0) { + Logger.error(TAG, 'player inputFile size is 0'); + } + this.isTranscoding = true; + player.playNative(inputFile.fd, outputFile.fd, Const.DEFAULT_VALUE, inputFileState.size, + this.videoDataModel.videoCodecMime, this.videoDataModel.width, this.videoDataModel.height, + this.videoDataModel.frameRate, this.videoDataModel.bitRate, + () => { + Logger.info(TAG, 'player JSCallback'); + this.isTranscoding = false; + this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogId); + fileIo.close(inputFile); + this.pageStack.pushPathByName('VideoPlayer', ''); + }) + } +} \ No newline at end of file diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 new file mode 100644 index 0000000..4faca83 --- /dev/null +++ b/entry/src/main/module.json5 @@ -0,0 +1,52 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:icon", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ], + "requestPermissions": [ + ] + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/color.json b/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000..9461eae --- /dev/null +++ b/entry/src/main/resources/base/element/color.json @@ -0,0 +1,12 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + }, + { + "name": "set_button_background", + "value": "#E5E7E9" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/float.json b/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000..d7b1000 --- /dev/null +++ b/entry/src/main/resources/base/element/float.json @@ -0,0 +1,72 @@ +{ + "float": [ + { + "name": "set_image_width", + "value": "24vp" + }, + { + "name": "set_image_height", + "value": "24vp" + }, + { + "name": "set_button_width", + "value": "40vp" + }, + { + "name": "set_button_height", + "value": "40vp" + }, + { + "name": "set_button_margin_right", + "value": "16vp" + }, + { + "name": "set_row_height", + "value": "40vp" + }, + { + "name": "set_row_margin_top", + "value": "8vp" + }, + { + "name": "window_margin_top", + "value": "150vp" + }, + { + "name": "index_button_width", + "value": "288vp" + }, + { + "name": "index_button_height", + "value": "40vp" + }, + { + "name": "button_margin_bottom", + "value": "12vp" + }, + { + "name": "last_button_margin_bottom", + "value": "16vp" + }, + { + "name": "index_column_height", + "value": "200vp" + }, + { + "name": "recorder_time_margin_bottom", + "value": "500vp" + }, + { + "name": "recorder_image_width", + "value": "60vp" + }, + { + "name": "recorder_image_height", + "value": "60vp" + }, + { + "name": "recorder_image_margin_top", + "value": "550vp" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..cc5c5c1 --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,64 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "AVCodecVideo" + }, + { + "name": "alert", + "value": "None video selected" + }, + { + "name": "full_width", + "value": "100%" + }, + { + "name": "full_height", + "value": "100%" + }, + { + "name": "reason", + "value": "allows an app to use the microphone" + }, + { + "name": "camera_reason", + "value": "allows an app to use the camera" + }, + { + "name": "select_video", + "value": "Select Video" + }, + { + "name": "config_video", + "value": "Configuration" + }, + { + "name": "start_transcoding", + "value": "Video Transcoding" + }, + { + "name": "alert_setting", + "value": "The setting is not supported by the current device." + }, + { + "name": "saveButtonNote", + "value": "Allow AVCodecVideo to save captured video to gallery?" + }, + { + "name": "saveButtonCancel", + "value": "Cancel" + }, + { + "name": "saveButtonTitle", + "value": "Confirm the video storage location" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/camera_pause_video_4x.png b/entry/src/main/resources/base/media/camera_pause_video_4x.png new file mode 100644 index 0000000000000000000000000000000000000000..84059db71f51f23c8f1151416a35807c245065c7 GIT binary patch literal 9196 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 aqNvMpY4^9FF(-ebG0-!Hh%iD0Q^} literal 0 HcmV?d00001 diff --git a/entry/src/main/resources/base/media/gearshape.svg b/entry/src/main/resources/base/media/gearshape.svg new file mode 100644 index 0000000..75dde1a --- /dev/null +++ b/entry/src/main/resources/base/media/gearshape.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/icon.png b/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a354ba394b7575c510700847d6473ecd0b1e740 GIT binary patch literal 3639 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 C?mQa+ literal 0 HcmV?d00001 diff --git a/entry/src/main/resources/base/media/pause.svg b/entry/src/main/resources/base/media/pause.svg new file mode 100644 index 0000000..e5c4823 --- /dev/null +++ b/entry/src/main/resources/base/media/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/play.svg b/entry/src/main/resources/base/media/play.svg new file mode 100644 index 0000000..d7ad35e --- /dev/null +++ b/entry/src/main/resources/base/media/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/transcoding_success.svg b/entry/src/main/resources/base/media/transcoding_success.svg new file mode 100644 index 0000000..521ba42 --- /dev/null +++ b/entry/src/main/resources/base/media/transcoding_success.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/entry/src/main/resources/base/profile/backup_config.json b/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000..78f40ae --- /dev/null +++ b/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/entry/src/main/resources/en_US/element/string.json b/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000..0460a0d --- /dev/null +++ b/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,64 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "AVCodecTranscoder" + }, + { + "name": "alert", + "value": "None video selected" + }, + { + "name": "full_width", + "value": "100%" + }, + { + "name": "full_height", + "value": "100%" + }, + { + "name": "reason", + "value": "allows an app to use the microphone" + }, + { + "name": "camera_reason", + "value": "allows an app to use the camera" + }, + { + "name": "select_video", + "value": "Select Video" + }, + { + "name": "config_video", + "value": "Configuration" + }, + { + "name": "start_transcoding", + "value": "Start Transcoding" + }, + { + "name": "alert_setting", + "value": "The setting is not supported by the current device." + }, + { + "name": "saveButtonNote", + "value": "Allow AVCodecVideo to save captured video to gallery?" + }, + { + "name": "saveButtonCancel", + "value": "Cancel" + }, + { + "name": "saveButtonTitle", + "value": "Confirm the video storage location" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..a43739a --- /dev/null +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,65 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "AVCodecTranscoder" + }, + { + "name": "full_width", + "value": "100%" + }, + { + "name": "full_height", + "value": "100%" + }, + { + "name": "alert", + "value": "未选择视频!" + }, + + { + "name": "reason", + "value": "允许应用使用麦克风" + }, + { + "name": "camera_reason", + "value": "允许应用使用相机" + }, + { + "name": "select_video", + "value": "选择视频文件" + }, + { + "name": "config_video", + "value": "参数设置" + }, + { + "name": "start_transcoding", + "value": "开始转码" + }, + { + "name": "alert_setting", + "value": "当前设备不支持该设置。" + }, + { + "name": "saveButtonNote", + "value": "是否允许AVCodecVideo保存拍摄的视频到图库?" + }, + { + "name": "saveButtonCancel", + "value": "取消" + }, + { + "name": "saveButtonTitle", + "value": "视频保存位置确认" + } + ] +} \ No newline at end of file diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..06b2783 --- /dev/null +++ b/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.0", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/hvigorfile.ts b/hvigorfile.ts new file mode 100644 index 0000000..f3cb9f1 --- /dev/null +++ b/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/oh-package.json5 b/oh-package.json5 new file mode 100644 index 0000000..c87d871 --- /dev/null +++ b/oh-package.json5 @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +{ + "modelVersion": "5.0.0", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": {} +} diff --git a/screenshots/device/AVCodec_Index.png b/screenshots/device/AVCodec_Index.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2ba8eed0f373c997b582e7a3bb0b1514e4960b GIT binary patch literal 40122 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%0D_cKvv6g4(qf_-Q^YaIy{`~sB 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 literal 0 HcmV?d00001 -- Gitee From ba9385e002cf3b31ff52804dbeb3f51aabd49119 Mon Sep 17 00:00:00 2001 From: Ryan <865833921@qq.com> Date: Fri, 27 Jun 2025 09:46:50 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 49 ++++++------------------------ entry/src/main/ets/pages/Index.ets | 2 +- 2 files changed, 10 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index ddbc99e..da407c1 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,6 @@ 3. 点击开始转码后即可开始转码。 4. 在转码完成后,会跳转到下一个页面,可以查看转码完成的视频。 - - ### 工程目录 ``` @@ -84,44 +82,15 @@ ### 具体实现 -#### *录制* -##### UI层 -1. 在UI层Index页面,用户点击“录制”后,会拉起半模态界面,用户确认保存录制文件到图库。录制结束后,文件会存放于图库。 -2. 选择好文件后,会用刚刚打开的fd,和用户预设的录制参数,掉起ArkTS的initNative,待初始化结束后,调用OH_NativeWindow_GetSurfaceId接口,得到NativeWindow的surfaceId,并把surfaceId回调回UI层。 -3. UI层拿到编码器给的surfaceId后,调起页面路由,携带该surfaceId,跳转到Recorder页面; -4. 录制页面XComponent构建时,会调起.onLoad()方法,此方法首先会拿到XComponent的surfaceId,然后调起createDualChannelPreview(),此函数会建立一个相机生产,XComponent和编码器的surface消费的生产消费模型。 - -##### Native层 -1. 进入录制界面后,编码器启动,开始对UI层相机预览流进行编码。 -2. 编码器每编码成功一帧,sample_callback.cpp的输出回调OnNewOutputBuffer()就会调起一次,此时用户会拿到AVCodec框架给出的OH_AVBuffer; -3. 在输出回调中,用户需手动把帧buffer、index存入输出队列中,并通知输出线程解锁; -4. 在输出线程中,把上一步的帧信息储存为bufferInfo后,pop出队; -5. 在输出线程中,使用上一步的bufferInfo,调用封装接口WriteSample后,这一帧被封装入MP4中; -6. 最后调用FreeOutputBuffer接口后,这一帧buffer释放回AVCodec框架,实现buffer轮转。 - -#### *播放* -##### UI层 -1. 在UI层Index页面,用户点击播放按钮后,触发点击事件,调起selectFile()函数,该函数会调起图库的选择文件模块,拿到用户选取文件的路径; -2. 用户选择文件成功后,调起play()函数,该函数会根据上一步获取到的路径,打开一个文件,并获取到该文件的大小,改变按钮状态为不可用,之后调起ArkTS层暴露给应用层的playNative()接口; -3. 根据playNative字段,调起PlayerNative::Play()函数,此处会注册播放结束的回调。 -4. 播放结束时,Callback()中napi_call_function()接口调起,通知应用层,恢复按钮状态为可用。 - -##### ArkTS层 -1. 在PlayerNative.cpp的Init()中调用PluginManager()中的Export()方法,注册OnSurfaceCreatedCB()回调,当屏幕上出现新的XComponent时,将其转换并赋给单例类PluginManager中的pluginWindow_; - -##### Native层 -1. 具体实现原理: - - 解码器Start后,解码器每拿到一帧,OnNeedInputBuffer就会被调起一次,AVCodec框架会给用户一个OH_AVBuffer。 - - 在输入回调中,用户需手动把帧buffer、index存入输入队列中,并同时输入线程解锁。 - - 在输入线程中,把上一步的帧信息储存为bufferInfo后,pop出队。 - - 在输入线程中,使用上一步的bufferInfo,调用ReadSample接口解封装帧数据。 - - 在输入线程中,使用解封装后的bufferInfo,调用解码的PushInputData接口,此时这片buffer用完,返回框架,实现buffer轮转。 - - PushInputData后,这一帧开始解码,每解码完成一帧,输出回调会被调起一次,用户需手动把帧buffer、index存入输出队列中。 - - 在输出线程中,把上一步的帧信息储存为bufferInfo后,pop出队。 - - 在输出线程中,调用FreeOutputData接口后,就会送显并释放buffer。释放的buffer会返回框架,实现buffer轮转。 -2. 解码器config阶段,OH_VideoDecoder_SetSurface接口的入参OHNativeWindow*,即为PluginManager中的pluginWindow_。 -3. 解码器config阶段,SetCallback接口,sample_callback.cpp的输入输出回调需将回调上来的帧buffer和index存入一个用户自定义容器sample_info.h中,方便后续操作。 -4. Player.cpp的Start()起两个专门用于输入和输出的线程。 +#### UI层 +1. 在UI层Index页面,用户点击“选择视频文件”后,会拉起半模态界面,用户确认需要转码的视频文件。 +2. 选择好文件后,用户需要设置的转码参数,保存并传递给Native侧,调用转码方法进行编解码。 + +#### Native层 +1. 在开始转码前,需要对环境进行初始化,包括解封装器、封装器、编码器、解码器。同时,将需要用到的上下文参数进行保存。 +2. 开启解码子线程,将视频数据进行解码,解码子线程包括输入子线程、输出子线程。在解码输入子线程中,用户需手动把帧buffer、index存入输入队列中,并通知解码其进行解码。 +3. 在解码输出线程中,将解码器解码后的视频数据进行数据拷贝。将拷贝的数据存入编码的输入队列,并同步释放编码的视频地址。 +4. 在编码输出线程中,将输出队列的bufferInfo进行出栈,并将对应的数据通过封装器写入到视频文件中。 ### 相关权限 diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 1d9fb3b..23e9404 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -94,7 +94,7 @@ struct Index { }) .then((photoSelectResult) => { this.selectFilePath = photoSelectResult.photoUris[0]; - if (this.selectFilePath === null) { + if (this.selectFilePath === null || this.selectFilePath === undefined) { this.getUIContext().getPromptAction().showToast({ message: $r('app.string.alert'), duration: Const.DURATION, -- Gitee From 5fd4554c9d502fcba7cee6feb8beb306c3e2bb60 Mon Sep 17 00:00:00 2001 From: Ryan <865833921@qq.com> Date: Fri, 27 Jun 2025 12:27:30 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entry/src/main/ets/pages/Index.ets | 1 - entry/src/main/ets/pages/VideoPlayer.ets | 331 +++++++++--------- entry/src/main/ets/pages/VideoTranscoding.ets | 168 +++++---- 3 files changed, 243 insertions(+), 257 deletions(-) diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 23e9404..3c800c1 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -68,7 +68,6 @@ struct Index { width: '100%', height: 40 }) - .margin({ bottom: 12 }) } .padding({ left: 16, diff --git a/entry/src/main/ets/pages/VideoPlayer.ets b/entry/src/main/ets/pages/VideoPlayer.ets index e0a2327..443aec8 100644 --- a/entry/src/main/ets/pages/VideoPlayer.ets +++ b/entry/src/main/ets/pages/VideoPlayer.ets @@ -33,201 +33,200 @@ export struct VideoPlayer { private transController: VideoController = new VideoController(); build() { - Row() { - Column() { - // Video area - Row() { - Button('转码前') - .onClick(() => { - this.tabsController.changeIndex(0); - this.currentIndex = 0; - this.changeIndex(); - }) - .fontColor(this.tabColorOne) - .backgroundColor(this.backgroundColorOne) - .margin(2) - .width(80) - Button('转码后') - .onClick(() => { - this.tabsController.changeIndex(1); - this.currentIndex = 1; - this.changeIndex(); - }) - .fontColor(this.tabColorTwo) - .backgroundColor(this.backgroundColorTwo) - .margin(2) - .width(80) - } - .borderRadius(20) - .backgroundColor('#E5E5E5') - .margin({ bottom: 16 }) - .width(168) + Column() { + // Video area + Row() { + Button('转码前') + .onClick(() => { + this.tabsController.changeIndex(1); + this.currentIndex = 1; + this.changeIndex(); + }) + .fontColor(this.tabColorTwo) + .backgroundColor(this.backgroundColorTwo) + .margin(2) + .width(80) + Button('转码后') + .onClick(() => { + this.tabsController.changeIndex(0); + this.currentIndex = 0; + this.changeIndex(); + }) + .fontColor(this.tabColorOne) + .backgroundColor(this.backgroundColorOne) + .margin(2) + .width(80) + } + .borderRadius(20) + .backgroundColor('#E5E5E5') + .margin({ bottom: 16 }) + .width(168) - Tabs({ index: this.currentIndex, controller: this.tabsController }) { - TabContent() { - Column() { - Stack() { - Video({ src: AppStorage.get('selectedFile'), controller: this.controller }) - .width('100%') - .height('100%') - .autoPlay(true) - .controls(false) - .objectFit(1) - .zIndex(0) - .onPrepared((event) => { - if (event) { - this.durationTime = event.duration - console.info(this.durationTime.toString()); - } - }) - .onUpdate((event) => { - if (event) { - this.currentTime = event.time + Tabs({ index: this.currentIndex, controller: this.tabsController }) { + TabContent() { + Column() { + Stack() { + Video({ src: 'file://' + AppStorage.get('transcodingFile'), controller: this.transController }) + .width('100%') + .height('100%') + .autoPlay(true) + .controls(false) + .objectFit(1) + .zIndex(0) + .onPrepared((event) => { + if (event) { + this.transDurationTime = event.duration + console.info(this.transDurationTime.toString()); + } + }) + .onUpdate((event) => { + if (event) { + this.transCurrentTime = event.time + } + }) + .onFinish(() => { + this.transIsStart = !this.transIsStart; + }) + .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) + + Row() { + Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) + .width(18) + .height(18) + .onClick(() => { + if (this.transIsStart) { + this.transController.pause(); + this.transIsStart = !this.transIsStart; + } else { + this.transController.start(); + this.transIsStart = !this.transIsStart; } }) - .onFinish(() => { - this.isStart = !this.isStart; - }) - .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) - - Row() { - Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) - .width(18) - .height(18) - .onClick(() => { - if (this.isStart) { - this.controller.pause(); - this.isStart = !this.isStart; - } else { - this.controller.start(); - this.isStart = !this.isStart; - } - }) - Text(formatVideoTime(this.currentTime)) - .fontColor(Color.White) - .fontSize(12) - .margin({ left: '12vp' }) - Slider({ - value: this.currentTime, - min: 0, - max: this.durationTime + Text(formatVideoTime(this.transCurrentTime)) + .fontColor(Color.White) + .fontSize(12) + .margin({ left: '12vp' }) + Slider({ + value: this.transCurrentTime, + min: 0, + max: this.transDurationTime + }) + .onChange((value: number, mode: SliderChangeMode) => { + // Set the video playback progress to jump to the value + this.transController.setCurrentTime(value); }) - .onChange((value: number, mode: SliderChangeMode) => { - // Set the video playback progress to jump to the value - this.controller.setCurrentTime(value); - }) - .layoutWeight(1) - Text(formatVideoTime(this.durationTime)) - .fontColor(Color.White) - .fontSize(12) - } - .padding({ left: '16vp', right: '16vp' }) - .zIndex(1) - .height(43) - .width('100%') + .layoutWeight(1) + Text(formatVideoTime(this.transDurationTime)) + .fontColor(Color.White) + .fontSize(12) } - .alignContent(Alignment.Bottom) + .padding({ left: '16vp', right: '16vp' }) + .zIndex(1) + .height(43) .width('100%') - .layoutWeight(1) } + .alignContent(Alignment.Bottom) .width('100%') .layoutWeight(1) } + .width('100%') + .layoutWeight(1) + } - TabContent() { - Column() { - Stack() { - Video({ src: 'file://' + AppStorage.get('transcodingFile'), controller: this.transController }) - .width('100%') - .height('100%') - .autoPlay(true) - .controls(false) - .objectFit(1) - .zIndex(0) - .onPrepared((event) => { - if (event) { - this.transDurationTime = event.duration - console.info(this.transDurationTime.toString()); - } - }) - .onUpdate((event) => { - if (event) { - this.transCurrentTime = event.time + TabContent() { + Column() { + Stack() { + Video({ src: AppStorage.get('selectedFile'), controller: this.controller }) + .width('100%') + .height('100%') + .autoPlay(true) + .controls(false) + .objectFit(1) + .zIndex(0) + .onPrepared((event) => { + if (event) { + this.durationTime = event.duration + console.info(this.durationTime.toString()); + } + }) + .onUpdate((event) => { + if (event) { + this.currentTime = event.time + } + }) + .onFinish(() => { + this.isStart = !this.isStart; + }) + .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) + + Row() { + Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) + .width(18) + .height(18) + .onClick(() => { + if (this.isStart) { + this.controller.pause(); + this.isStart = !this.isStart; + } else { + this.controller.start(); + this.isStart = !this.isStart; } }) - .onFinish(() => { - this.transIsStart = !this.transIsStart; - }) - .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) - - Row() { - Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) - .width(18) - .height(18) - .onClick(() => { - if (this.transIsStart) { - this.transController.pause(); - this.transIsStart = !this.transIsStart; - } else { - this.transController.start(); - this.transIsStart = !this.transIsStart; - } - }) - Text(formatVideoTime(this.transCurrentTime)) - .fontColor(Color.White) - .fontSize(12) - .margin({ left: '12vp' }) - Slider({ - value: this.transCurrentTime, - min: 0, - max: this.transDurationTime + Text(formatVideoTime(this.currentTime)) + .fontColor(Color.White) + .fontSize(12) + .margin({ left: '12vp' }) + Slider({ + value: this.currentTime, + min: 0, + max: this.durationTime + }) + .onChange((value: number, mode: SliderChangeMode) => { + // Set the video playback progress to jump to the value + this.controller.setCurrentTime(value); }) - .onChange((value: number, mode: SliderChangeMode) => { - // Set the video playback progress to jump to the value - this.transController.setCurrentTime(value); - }) - .layoutWeight(1) - Text(formatVideoTime(this.transDurationTime)) - .fontColor(Color.White) - .fontSize(12) - } - .padding({ left: '16vp', right: '16vp' }) - .zIndex(1) - .height(43) - .width('100%') + .layoutWeight(1) + Text(formatVideoTime(this.durationTime)) + .fontColor(Color.White) + .fontSize(12) } - .alignContent(Alignment.Bottom) + .padding({ left: '16vp', right: '16vp' }) + .zIndex(1) + .height(43) .width('100%') - .layoutWeight(1) } + .alignContent(Alignment.Bottom) .width('100%') .layoutWeight(1) } + .width('100%') + .layoutWeight(1) } - .barHeight(0) - .scrollable(false) - .barMode(BarMode.Fixed) - .height('80%') + } + .barHeight(0) + .scrollable(false) + .barMode(BarMode.Fixed) + .height('80%') - Blank() + Blank() - Column() { - Button('返回首页') - .width('100%') - .onClick(() => { - this.pageInfos.popToIndex(-1); - }) - } - .margin({ top: '12vp' }) - .justifyContent(FlexAlign.End) - .padding({ left: 16, right: 16, bottom: 16 }) - .width('100%') + Column() { + Button('返回首页') + .width('100%') + .onClick(() => { + this.pageInfos.popToIndex(-1); + }) } + .margin({ top: '12vp' }) + .justifyContent(FlexAlign.End) + .padding({ left: 16, right: 16, bottom: 16 }) .width('100%') } + .width('100%') .height('100%') + .justifyContent(FlexAlign.Start) } changeIndex() { diff --git a/entry/src/main/ets/pages/VideoTranscoding.ets b/entry/src/main/ets/pages/VideoTranscoding.ets index 3f54547..739e3ee 100644 --- a/entry/src/main/ets/pages/VideoTranscoding.ets +++ b/entry/src/main/ets/pages/VideoTranscoding.ets @@ -4,7 +4,7 @@ import { CommonConstants as Const } from '../common/CommonConstants'; import player from 'libplayer.so'; import Logger from '../common/utils/Logger'; import { fileIo } from '@kit.CoreFileKit'; -import { ImmersiveMode, LevelMode } from '@kit.ArkUI'; +import { LoadingDialog } from '@kit.ArkUI'; const TAG: string = Const.INDEX_TAG; @@ -20,6 +20,11 @@ export struct VideoTranscoding { private videoDataModel: VideoDataModel = new VideoDataModel(); private controller: VideoController = new VideoController(); customDialogId: number = 0; + dialogController: CustomDialogController = new CustomDialogController({ + builder: LoadingDialog({ + content: '视频转码中' + }) + }); @Builder customDialogComponent() { @@ -121,104 +126,87 @@ export struct VideoTranscoding { Blank() - Button($r('app.string.config_video')) - .id('config') - .onClick(() => { - this.getUIContext().showTextPickerDialog({ - defaultPickerItemHeight: Const.DEFAULT_PICKER_ITEM_HEIGHT, - selectedTextStyle: ({ - font: ({ - size: Const.SELECTED_TEXT_STYLE_FONT_SIZE - }) - }), - range: Const.RECORDER_INFO, - canLoop: false, - alignment: DialogAlignment.Center, - backgroundColor: Color.White, - backgroundBlurStyle: BlurStyle.BACKGROUND_ULTRA_THICK, - onAccept: (value: TextPickerResult) => { - switch (value.value[0]) { - case Const.VIDEO_MIMETYPE[0]: { - this.videoDataModel.setCodecFormat(Const.TRUE, Const.MIME_VIDEO_AVC); - break; - } - case Const.VIDEO_MIMETYPE[1]: { - this.videoDataModel.setCodecFormat(Const.FALSE, Const.MIME_VIDEO_HEVC); - break; + Column() { + Button($r('app.string.config_video')) + .id('config') + .onClick(() => { + this.getUIContext().showTextPickerDialog({ + defaultPickerItemHeight: Const.DEFAULT_PICKER_ITEM_HEIGHT, + selectedTextStyle: ({ + font: ({ + size: Const.SELECTED_TEXT_STYLE_FONT_SIZE + }) + }), + range: Const.RECORDER_INFO, + canLoop: false, + alignment: DialogAlignment.Center, + onAccept: (value: TextPickerResult) => { + switch (value.value[0]) { + case Const.VIDEO_MIMETYPE[0]: { + this.videoDataModel.setCodecFormat(Const.TRUE, Const.MIME_VIDEO_AVC); + break; + } + case Const.VIDEO_MIMETYPE[1]: { + this.videoDataModel.setCodecFormat(Const.FALSE, Const.MIME_VIDEO_HEVC); + break; + } + default: + break; } - default: - break; - } - switch (value.value[1]) { - case Const.VIDEO_RESOLUTION[0]: { - this.videoDataModel.setResolution(Const.VIDEO_WIDTH_4K, Const.VIDEO_HEIGHT_4K, - Const.BITRATE_VIDEO_30M); - break; + switch (value.value[1]) { + case Const.VIDEO_RESOLUTION[0]: { + this.videoDataModel.setResolution(Const.VIDEO_WIDTH_4K, Const.VIDEO_HEIGHT_4K, + Const.BITRATE_VIDEO_30M); + break; + } + case Const.VIDEO_RESOLUTION[1]: { + this.videoDataModel.setResolution(Const.VIDEO_WIDTH_1080P, Const.VIDEO_HEIGHT_1080P, + Const.BITRATE_VIDEO_20M); + break; + } + case Const.VIDEO_RESOLUTION[2]: { + this.videoDataModel.setResolution(Const.VIDEO_WIDTH_720P, Const.VIDEO_HEIGHT_720P, + Const.BITRATE_VIDEO_10M); + break; + } + default: + break; } - case Const.VIDEO_RESOLUTION[1]: { - this.videoDataModel.setResolution(Const.VIDEO_WIDTH_1080P, Const.VIDEO_HEIGHT_1080P, - Const.BITRATE_VIDEO_20M); - break; - } - case Const.VIDEO_RESOLUTION[2]: { - this.videoDataModel.setResolution(Const.VIDEO_WIDTH_720P, Const.VIDEO_HEIGHT_720P, - Const.BITRATE_VIDEO_10M); - break; - } - default: - break; - } - switch (value.value[2]) { - case Const.VIDEO_FRAMERATE[0]: { - this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_30FPS; - break; + switch (value.value[2]) { + case Const.VIDEO_FRAMERATE[0]: { + this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_30FPS; + break; + } + case Const.VIDEO_FRAMERATE[1]: { + this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_60FPS; + break; + } + default: + break; } - case Const.VIDEO_FRAMERATE[1]: { - this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_60FPS; - break; - } - default: - break; } - } - }); - + }); - }) - .size({ - width: '100%', - height: 40 - }) - .margin({ top: 12, bottom: 12 }) + }) + .size({ + width: '100%', + height: 40 + }) + .margin({ + bottom: 16 + }) - Button('开始转码') - .onClick(() => { - const node: FrameNode | null = this.getUIContext().getFrameNodeById("test_text") || null; - this.getUIContext().getPromptAction().openCustomDialog({ - builder: () => { - this.customDialogComponent(); - }, - levelMode: LevelMode.EMBEDDED, // 启用页面级弹出框 - levelUniqueId: node?.getUniqueId(), // 设置页面级弹出框所在页面的任意节点ID - immersiveMode: ImmersiveMode.EXTEND, // 设置页面级弹出框蒙层的显示模式 - }).then((dialogId: number) => { - this.customDialogId = dialogId; + Button('开始转码') + .onClick(() => { + this.dialogController.open(); + this.transcoding(); }) - this.transcoding(); - }) - .size({ - width: '100%', - height: $r('app.float.index_button_height') - }) - .margin({ bottom: 12 }) + .width('100%') + } + .padding(16) } - .padding({ - left: 16, - right: 16, - bottom: 16 - }) .width('100%') .height('100%') .justifyContent(FlexAlign.Start) @@ -243,7 +231,7 @@ export struct VideoTranscoding { () => { Logger.info(TAG, 'player JSCallback'); this.isTranscoding = false; - this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogId); + this.dialogController.close(); fileIo.close(inputFile); this.pageStack.pushPathByName('VideoPlayer', ''); }) -- Gitee From 09a6140e33405252389021f1294b2a0c97a8f8b8 Mon Sep 17 00:00:00 2001 From: Ryan <865833921@qq.com> Date: Fri, 27 Jun 2025 14:21:47 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 ------------- build-profile.json5 | 13 ------------ entry/src/main/ets/pages/Index.ets | 6 +++--- entry/src/main/ets/pages/VideoPlayer.ets | 3 +-- entry/src/main/ets/pages/VideoTranscoding.ets | 15 ++++++++++++++ .../main/resources/base/element/string.json | 16 +++++++-------- .../main/resources/en_US/element/string.json | 16 +++++++-------- .../main/resources/zh_CN/element/string.json | 20 +++++++++---------- 8 files changed, 45 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index da407c1..49322b0 100644 --- a/README.md +++ b/README.md @@ -3,20 +3,6 @@ ### 介绍 本实例基于AVCodec能力,实现了基于Buffer模式的视频转码功能。通过调用Native侧的编码器,解码器,以及封装和解封装功能,完成从视频解封装、解码、编码、封装的过程。基于本实例可以帮助开发者理解Buffer模式,并通过Buffer模式进行转码。 -### 播放支持的原子能力规格 -| 媒体格式 | 封装格式 | 码流格式 | -|------|:--------|:------------------------------------| -| 视频 | mp4 | 视频码流:H.264/H.265, 音频码流:AudioVivid | -| 视频 | mkv | 视频码流:H.264/H.265, 音频码流:aac/mp3/opus | -| 视频 | mpeg-ts | 视频码流:H.264, 音频码流:AudioVivid | - -### 录制支持的原子能力规格 - -| 封装格式 | 视频编解码类型 | -|------|-------------| -| mp4 | H.264/H.265 | - - ### 效果预览 | 应用主界面 | |------------------------------------------------------------| diff --git a/build-profile.json5 b/build-profile.json5 index 3f0c5d3..9d5bb0d 100644 --- a/build-profile.json5 +++ b/build-profile.json5 @@ -16,19 +16,6 @@ { "app": { "signingConfigs": [ - { - "name": "default", - "type": "HarmonyOS", - "material": { - "certpath": "C:\\Users\\xu\\.ohos\\config\\default_avcodec-buffer-mode_6-gFMlm-n7p76xoOjknDyNhxaNlAf4GDhMTN0eJLj_o=.cer", - "keyAlias": "debugKey", - "keyPassword": "0000001A28B2528533563CCD9BEFC82B94CADA661D1EAA4C06FF43B58A8801D23BC8C68AB3373AF03745", - "profile": "C:\\Users\\xu\\.ohos\\config\\default_avcodec-buffer-mode_6-gFMlm-n7p76xoOjknDyNhxaNlAf4GDhMTN0eJLj_o=.p7b", - "signAlg": "SHA256withECDSA", - "storeFile": "C:\\Users\\xu\\.ohos\\config\\default_avcodec-buffer-mode_6-gFMlm-n7p76xoOjknDyNhxaNlAf4GDhMTN0eJLj_o=.p12", - "storePassword": "0000001AD15B83BB0B7C6620498420192CB14C189615D040BB5D63B7DD07EB7521E8362E633C79835E07" - } - } ], "products": [ { diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 3c800c1..5bda9ae 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -33,14 +33,14 @@ struct Index { NavDestination() { VideoTranscoding() } - .title('视频转码') + .title($r('app.string.video_transcoding')) .backgroundColor('#F1F3F5') .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } else if (name === 'VideoPlayer') { NavDestination() { VideoPlayer() } - .title('视频播放') + .title($r('app.string.video_player')) .backgroundColor('#F1F3F5') .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } @@ -49,7 +49,7 @@ struct Index { build() { Navigation(this.pageStack) { Column() { - Text('基于Buffer模式进行转码') + Text($r('app.string.buffer_mode')) .width('100%') .fontColor('#E6000000') .fontSize(30) diff --git a/entry/src/main/ets/pages/VideoPlayer.ets b/entry/src/main/ets/pages/VideoPlayer.ets index 443aec8..a91c69c 100644 --- a/entry/src/main/ets/pages/VideoPlayer.ets +++ b/entry/src/main/ets/pages/VideoPlayer.ets @@ -34,7 +34,6 @@ export struct VideoPlayer { build() { Column() { - // Video area Row() { Button('转码前') .onClick(() => { @@ -90,7 +89,7 @@ export struct VideoPlayer { .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) Row() { - Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) + Image(this.transIsStart ? $r('app.media.pause') : $r('app.media.play')) .width(18) .height(18) .onClick(() => { diff --git a/entry/src/main/ets/pages/VideoTranscoding.ets b/entry/src/main/ets/pages/VideoTranscoding.ets index 739e3ee..b7184be 100644 --- a/entry/src/main/ets/pages/VideoTranscoding.ets +++ b/entry/src/main/ets/pages/VideoTranscoding.ets @@ -1,3 +1,18 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { formatVideoTime } from '../common/utils/TimeUtils'; import { VideoDataModel } from '../model/VideoDataModel'; import { CommonConstants as Const } from '../common/CommonConstants'; diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index cc5c5c1..156a926 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -17,20 +17,20 @@ "value": "None video selected" }, { - "name": "full_width", - "value": "100%" + "name": "reason", + "value": "allows an app to use the microphone" }, { - "name": "full_height", - "value": "100%" + "name": "video_transcoding", + "value": "Video Transcoding" }, { - "name": "reason", - "value": "allows an app to use the microphone" + "name": "video_player", + "value": "Video Player" }, { - "name": "camera_reason", - "value": "allows an app to use the camera" + "name": "buffer_mode", + "value": "Video Transcoding Based On Buffer Mode" }, { "name": "select_video", diff --git a/entry/src/main/resources/en_US/element/string.json b/entry/src/main/resources/en_US/element/string.json index 0460a0d..7eef904 100644 --- a/entry/src/main/resources/en_US/element/string.json +++ b/entry/src/main/resources/en_US/element/string.json @@ -17,20 +17,20 @@ "value": "None video selected" }, { - "name": "full_width", - "value": "100%" + "name": "reason", + "value": "allows an app to use the microphone" }, { - "name": "full_height", - "value": "100%" + "name": "video_transcoding", + "value": "Video Transcoding" }, { - "name": "reason", - "value": "allows an app to use the microphone" + "name": "video_player", + "value": "Video Player" }, { - "name": "camera_reason", - "value": "allows an app to use the camera" + "name": "buffer_mode", + "value": "Video Transcoding Based On Buffer Mode" }, { "name": "select_video", diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json index a43739a..f0494af 100644 --- a/entry/src/main/resources/zh_CN/element/string.json +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -12,14 +12,6 @@ "name": "EntryAbility_label", "value": "AVCodecTranscoder" }, - { - "name": "full_width", - "value": "100%" - }, - { - "name": "full_height", - "value": "100%" - }, { "name": "alert", "value": "未选择视频!" @@ -30,8 +22,16 @@ "value": "允许应用使用麦克风" }, { - "name": "camera_reason", - "value": "允许应用使用相机" + "name": "video_transcoding", + "value": "视频转码" + }, + { + "name": "video_player", + "value": "视频播放" + }, + { + "name": "buffer_mode", + "value": "基于Buffer模式进行转码" }, { "name": "select_video", -- Gitee From dd0048f093d9f17433afb41119b3aad562ee91dc Mon Sep 17 00:00:00 2001 From: Ryan <865833921@qq.com> Date: Wed, 2 Jul 2025 16:10:13 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=95=B4=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +- entry/oh-package.json5 | 2 +- entry/src/main/cpp/CMakeLists.txt | 8 +- .../src/main/cpp/capbilities/VideoEncoder.cpp | 16 +- entry/src/main/cpp/common/SampleCallback.cpp | 39 +++-- entry/src/main/cpp/common/SampleCallback.h | 3 +- .../Transcoding.cpp} | 150 +++++++++--------- .../Player.h => transcoding/Transcoding.h} | 14 +- .../TranscodingNative.cpp} | 20 +-- .../TranscodingNative.h} | 8 +- .../{libplayer => libtranscoding}/index.d.ts | 2 +- .../oh-package.json5 | 4 +- entry/src/main/ets/pages/VideoTranscoding.ets | 11 +- 13 files changed, 148 insertions(+), 143 deletions(-) rename entry/src/main/cpp/sample/{player/Player.cpp => transcoding/Transcoding.cpp} (86%) rename entry/src/main/cpp/sample/{player/Player.h => transcoding/Transcoding.h} (95%) rename entry/src/main/cpp/sample/{player/PlayerNative.cpp => transcoding/TranscodingNative.cpp} (85%) rename entry/src/main/cpp/sample/{player/PlayerNative.h => transcoding/TranscodingNative.h} (86%) rename entry/src/main/cpp/types/{libplayer => libtranscoding}/index.d.ts (96%) rename entry/src/main/cpp/types/{libplayer => libtranscoding}/oh-package.json5 (89%) diff --git a/README.md b/README.md index 49322b0..9a1d7c8 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,13 @@ │ │ ├──SampleCallback.h // 编解码回调定义 │ │ └──SampleInfo.h // 功能实现公共类 │ ├──sample // Native层 -│ │ └──player // Native层转码接口和实现 -│ │ ├──Player.cpp // Native层转码功能调用逻辑的实现 -│ │ ├──Player.h // Native层转码功能调用逻辑的接口 -│ │ ├──PlayerNative.cpp // Native层转码的入口 -│ │ └──PlayerNative.h +│ │ └──transcoding // Native层转码接口和实现 +│ │ ├──Transcoding.cpp // Native层转码功能调用逻辑的实现 +│ │ ├──Transcoding.h // Native层转码功能调用逻辑的接口 +│ │ ├──TranscodingNative.cpp // Native层转码的入口 +│ │ └──TranscodingNative.h │ ├──types // Native层暴露上来的接口 -│ │ └──libplayer // 转码模块暴露给UI层的接口 +│ │ └──libtranscoding // 转码模块暴露给UI层的接口 │ └──CMakeLists.txt // 编译入口 ├──ets // UI层 │ ├──common // 公共模块 @@ -51,7 +51,7 @@ │ ├──entrybackupability │ │ └──EntryBackupAbility.ets │ ├──model -│ │ └──CameraDataModel.ets // 相机参数数据类 +│ │ └──VideoDataModel.ets // 参数数据类 │ └──pages // EntryAbility 包含的页面 │ ├──Index.ets // 首页/视频选择页面 │ ├──VideoPlayer.ets // 视频播放页面 diff --git a/entry/oh-package.json5 b/entry/oh-package.json5 index fd87d3d..78b2917 100644 --- a/entry/oh-package.json5 +++ b/entry/oh-package.json5 @@ -21,6 +21,6 @@ "author": "", "license": "", "dependencies": { - "libplayer.so": "file:./src/main/cpp/types/libplayer" + "libtranscoding.so": "file:./src/main/cpp/types/libtranscoding" } } \ No newline at end of file diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt index d488abd..a8ae7fd 100644 --- a/entry/src/main/cpp/CMakeLists.txt +++ b/entry/src/main/cpp/CMakeLists.txt @@ -9,7 +9,7 @@ include_directories(${NATIVERENDER_ROOT_PATH} ${NATIVERENDER_ROOT_PATH}/common ${NATIVERENDER_ROOT_PATH}/common/dfx/err ${NATIVERENDER_ROOT_PATH}/common/dfx/log - ${NATIVERENDER_ROOT_PATH}/sample/player + ${NATIVERENDER_ROOT_PATH}/sample/transcoding ) set(BASE_LIBRARY @@ -18,8 +18,8 @@ set(BASE_LIBRARY libnative_media_venc.so libnative_media_acodec.so libnative_media_avdemuxer.so libnative_media_avsource.so libnative_media_avmuxer.so libohaudio.so ) -add_library(player SHARED sample/player/PlayerNative.cpp - sample/player/Player.cpp +add_library(transcoding SHARED sample/transcoding/TranscodingNative.cpp + sample/transcoding/Transcoding.cpp capbilities/Demuxer.cpp capbilities/VideoDecoder.cpp capbilities/AudioDecoder.cpp @@ -29,4 +29,4 @@ add_library(player SHARED sample/player/PlayerNative.cpp common/SampleCallback.cpp ) -target_link_libraries(player PUBLIC ${BASE_LIBRARY}) \ No newline at end of file +target_link_libraries(transcoding PUBLIC ${BASE_LIBRARY}) \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/VideoEncoder.cpp b/entry/src/main/cpp/capbilities/VideoEncoder.cpp index b45e0fd..3872366 100644 --- a/entry/src/main/cpp/capbilities/VideoEncoder.cpp +++ b/entry/src/main/cpp/capbilities/VideoEncoder.cpp @@ -37,10 +37,6 @@ int32_t VideoEncoder::Config(SampleInfo &sampleInfo, CodecUserData *codecUserDat int32_t ret = Configure(sampleInfo); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Configure failed"); - // GetSurface from video encoder -// ret = GetSurface(sampleInfo); -// CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Get surface failed"); - // SetCallback for video encoder ret = SetCallback(codecUserData); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, @@ -105,7 +101,7 @@ int32_t VideoEncoder::SetCallback(CodecUserData *codecUserData) { int32_t ret = OH_VideoEncoder_RegisterCallback(encoder_, {SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, - SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer}, + SampleCallback::EncOnNeedInputBuffer, SampleCallback::EncOnNewOutputBuffer}, codecUserData); CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret); @@ -143,8 +139,7 @@ int32_t VideoEncoder::Configure(const SampleInfo &sampleInfo) { AVCODEC_SAMPLE_LOGI("BitRate Mode: %{public}d, BitRate: %{public}" PRId64 "kbps", sampleInfo.bitrateMode, sampleInfo.bitrate / 1024); AVCODEC_SAMPLE_LOGI("====== VideoEncoder config ======"); - - // [Start set_encoder] + // Setting the Encoder int ret = OH_VideoEncoder_Configure(encoder_, format); // [End set_encoder] @@ -156,13 +151,6 @@ int32_t VideoEncoder::Configure(const SampleInfo &sampleInfo) { } // [End camera_AVCodec] -int32_t VideoEncoder::GetSurface(SampleInfo &sampleInfo) { - int32_t ret = OH_VideoEncoder_GetSurface(encoder_, &sampleInfo.window); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK && sampleInfo.window, AVCODEC_SAMPLE_ERR_ERROR, - "Get surface failed, ret: %{public}d", ret); - return AVCODEC_SAMPLE_ERR_OK; -} - int32_t VideoEncoder::PushInputBuffer(CodecBufferInfo &info) { CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); int32_t ret = OH_VideoEncoder_PushInputBuffer(encoder_, info.bufferIndex); diff --git a/entry/src/main/cpp/common/SampleCallback.cpp b/entry/src/main/cpp/common/SampleCallback.cpp index 82d550e..b531c52 100644 --- a/entry/src/main/cpp/common/SampleCallback.cpp +++ b/entry/src/main/cpp/common/SampleCallback.cpp @@ -89,37 +89,56 @@ void SampleCallback::OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVB return; } (void)codec; - CodecUserData *codecUserData = static_cast(userData); - if (codecUserData->isEncFirstFrame) { + std::unique_lock lock(codecUserData->inputMutex); + codecUserData->inputBufferInfoQueue.emplace(index, buffer); + codecUserData->inputCond.notify_all(); +} + +void SampleCallback::OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) { + if (userData == nullptr) { + return; + } + CodecUserData *codecUserData = static_cast(userData); + if(codecUserData->isDecFirstFrame) { OH_AVFormat *format = OH_VideoDecoder_GetOutputDescription(codec); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &codecUserData->width); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &codecUserData->height); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_STRIDE, &codecUserData->widthStride); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_SLICE_HEIGHT, &codecUserData->heightStride); OH_AVFormat_Destroy(format); - codecUserData->isEncFirstFrame = false; + codecUserData->isDecFirstFrame = false; } - std::unique_lock lock(codecUserData->inputMutex); - codecUserData->inputBufferInfoQueue.emplace(index, buffer); - codecUserData->inputCond.notify_all(); + std::unique_lock lock(codecUserData->outputMutex); + codecUserData->outputBufferInfoQueue.emplace(index, buffer); + codecUserData->outputCond.notify_all(); } -void SampleCallback::OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) { +void SampleCallback::EncOnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) { if (userData == nullptr) { return; } - (void)codec; CodecUserData *codecUserData = static_cast(userData); - if(codecUserData->isDecFirstFrame) { + if (codecUserData->isEncFirstFrame) { OH_AVFormat *format = OH_VideoDecoder_GetOutputDescription(codec); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &codecUserData->width); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &codecUserData->height); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_STRIDE, &codecUserData->widthStride); OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_SLICE_HEIGHT, &codecUserData->heightStride); OH_AVFormat_Destroy(format); - codecUserData->isDecFirstFrame = false; + codecUserData->isEncFirstFrame = false; + } + std::unique_lock lock(codecUserData->inputMutex); + codecUserData->inputBufferInfoQueue.emplace(index, buffer); + codecUserData->inputCond.notify_all(); +} + +void SampleCallback::EncOnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData) { + if (userData == nullptr) { + return; } + (void)codec; + CodecUserData *codecUserData = static_cast(userData); std::unique_lock lock(codecUserData->outputMutex); codecUserData->outputBufferInfoQueue.emplace(index, buffer); codecUserData->outputCond.notify_all(); diff --git a/entry/src/main/cpp/common/SampleCallback.h b/entry/src/main/cpp/common/SampleCallback.h index 4439deb..fa0736a 100644 --- a/entry/src/main/cpp/common/SampleCallback.h +++ b/entry/src/main/cpp/common/SampleCallback.h @@ -32,7 +32,8 @@ public: static void OnCodecFormatChange(OH_AVCodec *codec, OH_AVFormat *format, void *userData); static void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); static void OnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); - static void DecOnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); + static void EncOnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); + static void EncOnNewOutputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); }; #endif // AVCODEC_SAMPLE_CALLBACK_H \ No newline at end of file diff --git a/entry/src/main/cpp/sample/player/Player.cpp b/entry/src/main/cpp/sample/transcoding/Transcoding.cpp similarity index 86% rename from entry/src/main/cpp/sample/player/Player.cpp rename to entry/src/main/cpp/sample/transcoding/Transcoding.cpp index 9a6f282..0921078 100644 --- a/entry/src/main/cpp/sample/player/Player.cpp +++ b/entry/src/main/cpp/sample/transcoding/Transcoding.cpp @@ -13,14 +13,14 @@ * limitations under the License. */ -#include "Player.h" +#include "Transcoding.h" #include "AVCodecSampleLog.h" #include "dfx/error/AVCodecSampleError.h" #include "multimedia/player_framework/native_avbuffer.h" #include #undef LOG_TAG -#define LOG_TAG "player" +#define LOG_TAG "transcoding" namespace { constexpr int BALANCE_VALUE = 2; @@ -28,15 +28,15 @@ using namespace std::chrono_literals; constexpr int8_t YUV420_SAMPLE_RATIO = 2; } // namespace -Player::~Player() { Player::StartDecRelease(); } +Transcoding::~Transcoding() { Transcoding::StartDecRelease(); } -int32_t Player::CreateAudioDecoder() { +int32_t Transcoding::CreateAudioDecoder() { AVCODEC_SAMPLE_LOGW("audio mime:%{public}s", sampleInfo_.audioCodecMime.c_str()); int32_t ret = audioDecoder_->Create(sampleInfo_.audioCodecMime); return ret; } -int32_t Player::CreateVideoDecoder() { +int32_t Transcoding::CreateVideoDecoder() { AVCODEC_SAMPLE_LOGW("video mime:%{public}s", sampleInfo_.videoCodecMime.c_str()); int32_t ret = videoDecoder_->Create(sampleInfo_.videoCodecMime); if (ret != AVCODEC_SAMPLE_ERR_OK) { @@ -49,7 +49,7 @@ int32_t Player::CreateVideoDecoder() { return AVCODEC_SAMPLE_ERR_OK; } -int32_t Player::Init(SampleInfo &sampleInfo) { +int32_t Transcoding::Init(SampleInfo &sampleInfo) { std::unique_lock lock(mutex_); CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr && audioDecoder_ == nullptr, @@ -76,7 +76,7 @@ int32_t Player::Init(SampleInfo &sampleInfo) { return AVCODEC_SAMPLE_ERR_OK; } -int32_t Player::InitDecoder() { +int32_t Transcoding::InitDecoder() { CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr && audioDecoder_ == nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); @@ -101,7 +101,7 @@ int32_t Player::InitDecoder() { return ret; } -int32_t Player::CreateAudioEncoder() { +int32_t Transcoding::CreateAudioEncoder() { int32_t ret = audioEncoder_->Create(sampleInfo_.audioCodecMime); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create audio encoder(%{public}s) failed", sampleInfo_.audioCodecMime.c_str()); @@ -114,7 +114,7 @@ int32_t Player::CreateAudioEncoder() { return AVCODEC_SAMPLE_ERR_OK; } -int32_t Player::CreateVideoEncoder() { +int32_t Transcoding::CreateVideoEncoder() { int32_t ret = videoEncoder_->Create(sampleInfo_.videoCodecMime); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed"); @@ -125,7 +125,7 @@ int32_t Player::CreateVideoEncoder() { return AVCODEC_SAMPLE_ERR_OK; } -int32_t Player::InitEncoder() { +int32_t Transcoding::InitEncoder() { CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); CHECK_AND_RETURN_RET_LOG(muxer_ == nullptr && videoEncoder_ == nullptr && audioEncoder_ == nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); @@ -153,7 +153,7 @@ int32_t Player::InitEncoder() { } -int32_t Player::Start() { +int32_t Transcoding::Start() { std::unique_lock lock(mutex_); int32_t ret; CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); @@ -167,8 +167,8 @@ int32_t Player::Start() { return AVCODEC_SAMPLE_ERR_ERROR; } isStarted_ = true; - videoDecInputThread_ = std::make_unique(&Player::VideoDecInputThread, this); - videoDecOutputThread_ = std::make_unique(&Player::VideoDecOutputThread, this); + videoDecInputThread_ = std::make_unique(&Transcoding::VideoDecInputThread, this); + videoDecOutputThread_ = std::make_unique(&Transcoding::VideoDecOutputThread, this); if (videoDecInputThread_ == nullptr || videoDecOutputThread_ == nullptr) { AVCODEC_SAMPLE_LOGE("Create thread failed"); @@ -185,7 +185,7 @@ int32_t Player::Start() { CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Muxer start failed"); ret = videoEncoder_->Start(); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder start failed"); - videoEncOutputThread_ = std::make_unique(&Player::VideoEncOutputThread, this); + videoEncOutputThread_ = std::make_unique(&Transcoding::VideoEncOutputThread, this); if (videoEncOutputThread_ == nullptr) { AVCODEC_SAMPLE_LOGE("Create thread failed"); StartEncRelease(); @@ -202,8 +202,8 @@ int32_t Player::Start() { // return AVCODEC_SAMPLE_ERR_ERROR; // } // isStarted_ = true; -// audioDecInputThread_ = std::make_unique(&Player::AudioDecInputThread, this); -// audioDecOutputThread_ = std::make_unique(&Player::AudioDecOutputThread, this); +// audioDecInputThread_ = std::make_unique(&Transcoding::AudioDecInputThread, this); +// audioDecOutputThread_ = std::make_unique(&Transcoding::AudioDecOutputThread, this); // if (audioDecInputThread_ == nullptr || audioDecOutputThread_ == nullptr) { // AVCODEC_SAMPLE_LOGE("Create thread failed"); // lock.unlock(); @@ -221,7 +221,7 @@ int32_t Player::Start() { return AVCODEC_SAMPLE_ERR_OK; } -void Player::Stop() { +void Transcoding::Stop() { if (!isReleased_) { isReleased_ = true; DecRelease(); @@ -229,7 +229,7 @@ void Player::Stop() { StartEncRelease(); } -void Player::StartDecRelease() { +void Transcoding::StartDecRelease() { AVCODEC_SAMPLE_LOGI("start release"); std::unique_lock lock(doneMutex); doneCond_.wait(lock, [this]() { return isAudioDone.load(); }); @@ -240,7 +240,7 @@ void Player::StartDecRelease() { } } -void Player::ReleaseThread() { +void Transcoding::ReleaseThread() { if (videoDecInputThread_ && videoDecInputThread_->joinable()) { videoDecInputThread_->detach(); videoDecInputThread_.reset(); @@ -259,7 +259,7 @@ void Player::ReleaseThread() { } } -void Player::DecRelease() { +void Transcoding::DecRelease() { std::lock_guard lock(mutex_); ReleaseThread(); @@ -295,14 +295,14 @@ void Player::DecRelease() { AVCODEC_SAMPLE_LOGI("Succeed"); } -void Player::StartEncRelease() { +void Transcoding::StartEncRelease() { if (releaseEncThread_ == nullptr) { AVCODEC_SAMPLE_LOGI("Start release CodecTest"); - releaseEncThread_ = std::make_unique(&Player::EncRelease, this); + releaseEncThread_ = std::make_unique(&Transcoding::EncRelease, this); } } -void Player::EncRelease() { +void Transcoding::EncRelease() { std::lock_guard lock(mutex_); isStarted_ = false; if (videoEncOutputThread_ && videoEncOutputThread_->joinable()) { @@ -340,11 +340,6 @@ void Player::EncRelease() { audioEncoder_.reset(); AVCODEC_SAMPLE_LOGI("Audio encoder release successful"); } -// if (audioCapturer_ != nullptr) { -// audioCapturer_->AudioCapturerRelease(); -// audioCapturer_.reset(); -// AVCODEC_SAMPLE_LOGI("Audio Capturer release successful"); -// } if (audioEncContext_ != nullptr) { delete audioEncContext_; audioEncContext_ = nullptr; @@ -357,7 +352,7 @@ void Player::EncRelease() { AVCODEC_SAMPLE_LOGI("Succeed"); } -void Player::CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBufferInfo &bufferInfo) { +void Transcoding::CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBufferInfo &bufferInfo) { auto &info = sampleInfo_; int32_t videoWidth = videoDecContext_->width * @@ -367,41 +362,47 @@ void Player::CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBufferInfo int32_t uvStride = stride / YUV420_SAMPLE_RATIO; int32_t size = 0; uint8_t *tempBufferAddr = encBufferInfo.bufferAddr; - - // copy Y - for (int32_t row = 0; row < videoDecContext_->height; row++) { - std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, videoWidth); - tempBufferAddr += videoWidth; - bufferInfo.bufferAddr += stride; - } + size += videoDecContext_->height * videoWidth; - bufferInfo.bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) * stride; - - // copy U - for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { - std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); - tempBufferAddr += uvWidth; - bufferInfo.bufferAddr += uvStride; + size += videoDecContext_->height * uvWidth; + if (videoWidth == videoDecContext_->widthStride && videoDecContext_->heightStride == videoDecContext_->height) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, size); + } else { + // copy Y + for (int32_t row = 0; row < videoDecContext_->height; row++) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, videoWidth); + tempBufferAddr += videoWidth; + bufferInfo.bufferAddr += stride; + } + bufferInfo.bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) * stride; + + // copy U + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); + tempBufferAddr += uvWidth; + bufferInfo.bufferAddr += uvStride; + } + bufferInfo.bufferAddr += + (videoDecContext_->heightStride - videoDecContext_->height) / YUV420_SAMPLE_RATIO * uvStride; + + // copy V + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); + tempBufferAddr += uvWidth; + bufferInfo.bufferAddr += uvStride; + } } - bufferInfo.bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) / YUV420_SAMPLE_RATIO * uvStride; - // copy V - for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { - std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); - tempBufferAddr += uvWidth; - bufferInfo.bufferAddr += uvStride; - } - - size += videoDecContext_->height * uvWidth; + encBufferInfo.attr.size = size; encBufferInfo.attr.flags = bufferInfo.attr.flags; encBufferInfo.attr.offset = bufferInfo.attr.offset; encBufferInfo.attr.pts = bufferInfo.attr.pts; - encBufferInfo.attr.size = size; + tempBufferAddr = nullptr; delete tempBufferAddr; } -void Player::VideoDecInputThread() { +void Transcoding::VideoDecInputThread() { while (true) { CHECK_AND_BREAK_LOG(isStarted_, "Decoder input thread out"); std::unique_lock lock(videoDecContext_->inputMutex); @@ -422,15 +423,15 @@ void Player::VideoDecInputThread() { int32_t ret = videoDecoder_->PushInputBuffer(bufferInfo); CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Push data failed, thread out"); - CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "VideoDecInputThread Catch EOS, thread out"); + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), + "VideoDecInputThread Catch EOS, thread out"); } } -void Player::VideoDecOutputThread() { +void Transcoding::VideoDecOutputThread() { isVideoDone = false; sampleInfo_.frameInterval = MICROSECOND / sampleInfo_.frameRate; while (true) { - thread_local auto lastPushTime = std::chrono::system_clock::now(); CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); std::unique_lock lock(videoDecContext_->outputMutex); bool condRet = videoDecContext_->outputCond.wait_for(lock, 5s, [this]() { @@ -455,23 +456,22 @@ void Player::VideoDecOutputThread() { CodecBufferInfo encBufferInfo = videoEncContext_->inputBufferInfoQueue.front(); videoEncContext_->inputBufferInfoQueue.pop(); videoEncContext_->inputFrameCount++; - - - AVCODEC_SAMPLE_LOGW("Out bufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}" PRId64, - bufferInfo.attr.flags, bufferInfo.attr.offset, bufferInfo.attr.pts, - bufferInfo.attr.size); - + AVCODEC_SAMPLE_LOGW( + "Out bufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}" PRId64, + bufferInfo.attr.flags, bufferInfo.attr.offset, bufferInfo.attr.pts, bufferInfo.attr.size); + + encBufferInfo.bufferAddr = OH_AVBuffer_GetAddr(reinterpret_cast(encBufferInfo.buffer)); bufferInfo.bufferAddr = OH_AVBuffer_GetAddr(reinterpret_cast(bufferInfo.buffer)); CopyStrideYUV420SP(encBufferInfo, bufferInfo); - - AVCODEC_SAMPLE_LOGW("Out encBufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}d" PRId64, - encBufferInfo.attr.flags, encBufferInfo.attr.offset, encBufferInfo.attr.pts, - encBufferInfo.attr.size); - + + AVCODEC_SAMPLE_LOGW( + "Out encBufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}d" PRId64, + encBufferInfo.attr.flags, encBufferInfo.attr.offset, encBufferInfo.attr.pts, encBufferInfo.attr.size); + OH_AVBuffer_SetBufferAttr(reinterpret_cast(encBufferInfo.buffer), &encBufferInfo.attr); - + // 释放解码输出队列资源 int32_t ret = videoDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, false); CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Decoder output thread out"); @@ -483,15 +483,12 @@ void Player::VideoDecOutputThread() { AVCODEC_SAMPLE_LOGW("VideoDecOutputThread Catch EOS, thread out" PRId64); break; } - - std::this_thread::sleep_until(lastPushTime + std::chrono::microseconds(sampleInfo_.frameInterval)); - lastPushTime = std::chrono::system_clock::now(); } isVideoDone = true; StartDecRelease(); } -void Player::VideoEncOutputThread() { +void Transcoding::VideoEncOutputThread() { while (true) { std::unique_lock lock(videoEncContext_->outputMutex); bool condRet = videoEncContext_->outputCond.wait_for( @@ -502,7 +499,8 @@ void Player::VideoEncOutputThread() { CodecBufferInfo bufferInfo = videoEncContext_->outputBufferInfoQueue.front(); videoEncContext_->outputBufferInfoQueue.pop(); - CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "VideoEncOutputThread Catch EOS, thread out"); + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), + "VideoEncOutputThread Catch EOS, thread out"); lock.unlock(); if ((bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_SYNC_FRAME) || (bufferInfo.attr.flags == AVCODEC_BUFFER_FLAGS_NONE)) { @@ -525,7 +523,7 @@ void Player::VideoEncOutputThread() { } -void Player::AudioDecInputThread() { +void Transcoding::AudioDecInputThread() { while (true) { CHECK_AND_BREAK_LOG(isStarted_, "Decoder input thread out"); std::unique_lock lock(audioDecContext_->inputMutex); @@ -550,7 +548,7 @@ void Player::AudioDecInputThread() { } } -void Player::AudioDecOutputThread() { +void Transcoding::AudioDecOutputThread() { isAudioDone = false; while (true) { CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); diff --git a/entry/src/main/cpp/sample/player/Player.h b/entry/src/main/cpp/sample/transcoding/Transcoding.h similarity index 95% rename from entry/src/main/cpp/sample/player/Player.h rename to entry/src/main/cpp/sample/transcoding/Transcoding.h index 6bf38d2..81be6e0 100644 --- a/entry/src/main/cpp/sample/player/Player.h +++ b/entry/src/main/cpp/sample/transcoding/Transcoding.h @@ -34,14 +34,14 @@ #include "AudioEncoder.h" #include "Muxer.h" -class Player { +class Transcoding { public: - Player(){}; - ~Player(); + Transcoding(){}; + ~Transcoding(); - static Player &GetInstance() { - static Player player; - return player; + static Transcoding &GetInstance() { + static Transcoding transcoding; + return transcoding; } int32_t Init(SampleInfo &sampleInfo); @@ -106,4 +106,4 @@ private: static constexpr int64_t MICROSECOND = 1000000; }; -#endif // VIDEO_CODEC_PLAYER_H \ No newline at end of file +#endif \ No newline at end of file diff --git a/entry/src/main/cpp/sample/player/PlayerNative.cpp b/entry/src/main/cpp/sample/transcoding/TranscodingNative.cpp similarity index 85% rename from entry/src/main/cpp/sample/player/PlayerNative.cpp rename to entry/src/main/cpp/sample/transcoding/TranscodingNative.cpp index ab6448f..124c0d8 100644 --- a/entry/src/main/cpp/sample/player/PlayerNative.cpp +++ b/entry/src/main/cpp/sample/transcoding/TranscodingNative.cpp @@ -13,12 +13,12 @@ * limitations under the License. */ -#include "PlayerNative.h" +#include "TranscodingNative.h" #undef LOG_DOMAIN #undef LOG_TAG #define LOG_DOMAIN 0xFF00 -#define LOG_TAG "player" +#define LOG_TAG "transcoding" struct CallbackContext { napi_env env = nullptr; @@ -48,7 +48,7 @@ void Callback(void *asyncContext) { }); } -napi_value PlayerNative::Play(napi_env env, napi_callback_info info) { +napi_value TranscodingNative::Start(napi_env env, napi_callback_info info) { SampleInfo sampleInfo; size_t argc = 10; napi_value args[10] = {nullptr}; @@ -74,23 +74,23 @@ napi_value PlayerNative::Play(napi_env env, napi_callback_info info) { sampleInfo.playDoneCallback = &Callback; sampleInfo.playDoneCallbackData = asyncContext; - int32_t ret = Player::GetInstance().Init(sampleInfo); + int32_t ret = Transcoding::GetInstance().Init(sampleInfo); if (ret == AVCODEC_SAMPLE_ERR_OK) { - Player::GetInstance().Start(); + Transcoding::GetInstance().Start(); } return nullptr; } -napi_value PlayerNative::Stop(napi_env env, napi_callback_info info) { - Player::GetInstance().Stop(); +napi_value TranscodingNative::Stop(napi_env env, napi_callback_info info) { + Transcoding::GetInstance().Stop(); return nullptr; } EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) { napi_property_descriptor classProp[] = { - {"playNative", nullptr, PlayerNative::Play, nullptr, nullptr, nullptr, napi_default, nullptr}, - {"stopNative", nullptr, PlayerNative::Stop, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"startNative", nullptr, TranscodingNative::Start, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"stopNative", nullptr, TranscodingNative::Stop, nullptr, nullptr, nullptr, napi_default, nullptr}, }; napi_define_properties(env, exports, sizeof(classProp) / sizeof(classProp[0]), classProp); @@ -103,7 +103,7 @@ static napi_module PlayerModule = { .nm_flags = 0, .nm_filename = nullptr, .nm_register_func = Init, - .nm_modname = "player", + .nm_modname = "transcoding", .nm_priv = ((void *)0), .reserved = {0}, }; diff --git a/entry/src/main/cpp/sample/player/PlayerNative.h b/entry/src/main/cpp/sample/transcoding/TranscodingNative.h similarity index 86% rename from entry/src/main/cpp/sample/player/PlayerNative.h rename to entry/src/main/cpp/sample/transcoding/TranscodingNative.h index cb602d2..8178e3f 100644 --- a/entry/src/main/cpp/sample/player/PlayerNative.h +++ b/entry/src/main/cpp/sample/transcoding/TranscodingNative.h @@ -21,13 +21,13 @@ #include #include #include "napi/native_api.h" -#include "Player.h" +#include "Transcoding.h" #include "dfx/error/AVCodecSampleError.h" #include "AVCodecSampleLog.h" -class PlayerNative { +class TranscodingNative { public: - static napi_value Play(napi_env env, napi_callback_info info); + static napi_value Start(napi_env env, napi_callback_info info); static napi_value Stop(napi_env env, napi_callback_info info); }; -#endif // VIDEO_CODEC_SAMPLE_PLAYER_NATIVE_H \ No newline at end of file +#endif \ No newline at end of file diff --git a/entry/src/main/cpp/types/libplayer/index.d.ts b/entry/src/main/cpp/types/libtranscoding/index.d.ts similarity index 96% rename from entry/src/main/cpp/types/libplayer/index.d.ts rename to entry/src/main/cpp/types/libtranscoding/index.d.ts index dda6388..d826257 100644 --- a/entry/src/main/cpp/types/libplayer/index.d.ts +++ b/entry/src/main/cpp/types/libtranscoding/index.d.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -export const playNative: ( +export const startNative: ( inputFileFd: number, outputFileFd: number, inputFileOffset: number, diff --git a/entry/src/main/cpp/types/libplayer/oh-package.json5 b/entry/src/main/cpp/types/libtranscoding/oh-package.json5 similarity index 89% rename from entry/src/main/cpp/types/libplayer/oh-package.json5 rename to entry/src/main/cpp/types/libtranscoding/oh-package.json5 index d8dedda..5696eaf 100644 --- a/entry/src/main/cpp/types/libplayer/oh-package.json5 +++ b/entry/src/main/cpp/types/libtranscoding/oh-package.json5 @@ -14,8 +14,8 @@ */ { - "name": "libplayer.so", + "name": "libtranscoding.so", "types": "./index.d.ts", "version": "1.0.0", - "decription": "Player interface." + "decreiption": "Transcoding interface." } \ No newline at end of file diff --git a/entry/src/main/ets/pages/VideoTranscoding.ets b/entry/src/main/ets/pages/VideoTranscoding.ets index b7184be..03c47c8 100644 --- a/entry/src/main/ets/pages/VideoTranscoding.ets +++ b/entry/src/main/ets/pages/VideoTranscoding.ets @@ -16,7 +16,7 @@ import { formatVideoTime } from '../common/utils/TimeUtils'; import { VideoDataModel } from '../model/VideoDataModel'; import { CommonConstants as Const } from '../common/CommonConstants'; -import player from 'libplayer.so'; +import transcoding from 'libtranscoding.so'; import Logger from '../common/utils/Logger'; import { fileIo } from '@kit.CoreFileKit'; import { LoadingDialog } from '@kit.ArkUI'; @@ -58,7 +58,7 @@ export struct VideoTranscoding { .margin(8) .onClick(() => { this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogId); - player.stopNative(); + transcoding.stopNative(); }) }.padding(20) } @@ -233,18 +233,17 @@ export struct VideoTranscoding { let outputFile = fileIo.openSync(this.getUIContext().getHostContext()?.cacheDir + '/transcoding.mp4', fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); if (!inputFile) { - Logger.error(TAG, 'player inputFile is null'); + Logger.error(TAG, 'transcoding inputFile is null'); } let inputFileState = fileIo.statSync(inputFile.fd); if (inputFileState.size <= 0) { - Logger.error(TAG, 'player inputFile size is 0'); + Logger.error(TAG, 'transcoding inputFile size is 0'); } this.isTranscoding = true; - player.playNative(inputFile.fd, outputFile.fd, Const.DEFAULT_VALUE, inputFileState.size, + transcoding.startNative(inputFile.fd, outputFile.fd, Const.DEFAULT_VALUE, inputFileState.size, this.videoDataModel.videoCodecMime, this.videoDataModel.width, this.videoDataModel.height, this.videoDataModel.frameRate, this.videoDataModel.bitRate, () => { - Logger.info(TAG, 'player JSCallback'); this.isTranscoding = false; this.dialogController.close(); fileIo.close(inputFile); -- Gitee From 301975938dab95d301dddedfe242a1a8cbeea420 Mon Sep 17 00:00:00 2001 From: Ryan <865833921@qq.com> Date: Thu, 3 Jul 2025 15:27:55 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 +- entry/src/main/cpp/CMakeLists.txt | 2 - .../main/cpp/capbilities/AudioCapturer.cpp | 76 ----- .../src/main/cpp/capbilities/AudioDecoder.cpp | 129 -------- .../src/main/cpp/capbilities/AudioEncoder.cpp | 147 --------- entry/src/main/cpp/capbilities/Muxer.cpp | 17 +- .../src/main/cpp/capbilities/VideoEncoder.cpp | 4 +- .../cpp/capbilities/include/AudioCapturer.h | 40 --- .../cpp/capbilities/include/AudioDecoder.h | 43 --- .../cpp/capbilities/include/AudioEncoder.h | 48 --- entry/src/main/cpp/common/SampleCallback.cpp | 52 --- entry/src/main/cpp/common/SampleCallback.h | 9 - entry/src/main/cpp/common/SampleInfo.h | 37 --- .../cpp/sample/transcoding/Transcoding.cpp | 295 +++--------------- .../main/cpp/sample/transcoding/Transcoding.h | 16 +- .../sample/transcoding/TranscodingNative.cpp | 19 +- .../sample/transcoding/TranscodingNative.h | 5 - .../main/cpp/types/libtranscoding/index.d.ts | 2 - entry/src/main/ets/common/CommonConstants.ets | 2 +- entry/src/main/ets/model/VideoDataModel.ets | 6 +- entry/src/main/ets/pages/Index.ets | 233 +++++++++++--- entry/src/main/ets/pages/VideoPlayer.ets | 15 +- entry/src/main/ets/pages/VideoTranscoding.ets | 253 --------------- .../main/resources/base/element/string.json | 12 + .../main/resources/en_US/element/string.json | 12 + .../main/resources/rawfile/video_sample.mp4 | Bin 0 -> 10467141 bytes .../main/resources/zh_CN/element/string.json | 12 + screenshots/device/AVCodec_Index.png | Bin 40122 -> 133763 bytes 28 files changed, 302 insertions(+), 1205 deletions(-) delete mode 100644 entry/src/main/cpp/capbilities/AudioCapturer.cpp delete mode 100644 entry/src/main/cpp/capbilities/AudioDecoder.cpp delete mode 100644 entry/src/main/cpp/capbilities/AudioEncoder.cpp delete mode 100644 entry/src/main/cpp/capbilities/include/AudioCapturer.h delete mode 100644 entry/src/main/cpp/capbilities/include/AudioDecoder.h delete mode 100644 entry/src/main/cpp/capbilities/include/AudioEncoder.h delete mode 100644 entry/src/main/ets/pages/VideoTranscoding.ets create mode 100644 entry/src/main/resources/rawfile/video_sample.mp4 diff --git a/README.md b/README.md index 9a1d7c8..fce3d66 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # 基于Buffer模式进行视频转码 ### 介绍 -本实例基于AVCodec能力,实现了基于Buffer模式的视频转码功能。通过调用Native侧的编码器,解码器,以及封装和解封装功能,完成从视频解封装、解码、编码、封装的过程。基于本实例可以帮助开发者理解Buffer模式,并通过Buffer模式进行转码。 +本实例基于AVCodec能力,实现了Buffer模式下的视频转码功能。通过调用Native侧的编码器,解码器,以及封装和解封装功能,完成从视频解封装、解码、编码、封装的过程。基于本实例可以帮助开发者理解Buffer模式,并通过Buffer模式进行转码。 ### 效果预览 | 应用主界面 | @@ -9,10 +9,9 @@ | ![AVCodec_Index.png](screenshots/device/AVCodec_Index.png) | ### 使用说明 -1. 进入首页后,点击选择文件选择需要转码的视频。 -2. 给需要转码的视频配置对应的参数。 -3. 点击开始转码后即可开始转码。 -4. 在转码完成后,会跳转到下一个页面,可以查看转码完成的视频。 +1. 进入首页后,配置视频转码的参数。 +2. 点击开始转码后即可开始转码,等待转码完成。 +3. 在转码完成后,跳转到下一个页面,可以查看转码完成的视频。 ### 工程目录 @@ -20,8 +19,6 @@ ├──entry/src/main/cpp // Native层 │ ├──capbilities // 能力接口和实现 │ │ ├──include // 能力接口 -│ │ ├──AudioDecoder.cpp // 音频解码实现 -│ │ ├──AudioEncoder.cpp // 音频解码实现 │ │ ├──Demuxer.cpp // 解封装实现 │ │ ├──Muxer.cpp // 封装实现 │ │ ├──VideoDecoder.cpp // 视频解码实现 @@ -53,9 +50,8 @@ │ ├──model │ │ └──VideoDataModel.ets // 参数数据类 │ └──pages // EntryAbility 包含的页面 -│ ├──Index.ets // 首页/视频选择页面 -│ ├──VideoPlayer.ets // 视频播放页面 -│ └──VideoTranscoding.ets // 视频转码页面 +│ ├──Index.ets // 首页/视频转码页面 +│ └──VideoPlayer.ets // 视频播放页面 ├──resources // 用于存放应用所用到的资源文件 │ ├──base // 该目录下的资源文件会被赋予唯一的ID │ │ ├──element // 用于存放字体和颜色 @@ -69,8 +65,9 @@ ### 具体实现 #### UI层 -1. 在UI层Index页面,用户点击“选择视频文件”后,会拉起半模态界面,用户确认需要转码的视频文件。 -2. 选择好文件后,用户需要设置的转码参数,保存并传递给Native侧,调用转码方法进行编解码。 +1. 在ArkTS侧包含两个页面,首页和转码完成的页面。 +2. 首页是参数配置页面,调用了Video组件播放需要转码的视频,并调用showTextPickerDialog弹窗配置转码的参数。再点击转码的按钮后,调用Native转码的接口。 +3. 视频播放页面通过Video组件播放转码前和转码后的视频。 #### Native层 1. 在开始转码前,需要对环境进行初始化,包括解封装器、封装器、编码器、解码器。同时,将需要用到的上下文参数进行保存。 diff --git a/entry/src/main/cpp/CMakeLists.txt b/entry/src/main/cpp/CMakeLists.txt index a8ae7fd..bb2e559 100644 --- a/entry/src/main/cpp/CMakeLists.txt +++ b/entry/src/main/cpp/CMakeLists.txt @@ -22,10 +22,8 @@ add_library(transcoding SHARED sample/transcoding/TranscodingNative.cpp sample/transcoding/Transcoding.cpp capbilities/Demuxer.cpp capbilities/VideoDecoder.cpp - capbilities/AudioDecoder.cpp capbilities/Muxer.cpp capbilities/VideoEncoder.cpp - capbilities/AudioEncoder.cpp common/SampleCallback.cpp ) diff --git a/entry/src/main/cpp/capbilities/AudioCapturer.cpp b/entry/src/main/cpp/capbilities/AudioCapturer.cpp deleted file mode 100644 index 527c82d..0000000 --- a/entry/src/main/cpp/capbilities/AudioCapturer.cpp +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AudioCapturer.h" -#include "SampleCallback.h" - - -AudioCapturer::~AudioCapturer() -{ - AudioCapturerRelease(); -} - -// AudioCapturer Callback -static int32_t AudioCapturerOnReadData(OH_AudioCapturer *capturer, void *userData, void *buffer, int32_t bufferLen) -{ - (void)capturer; - CodecUserData *codecUserData = static_cast(userData); - if (codecUserData != nullptr) { - std::unique_lock lock(codecUserData->inputMutex); - codecUserData->WriteCache(buffer, bufferLen); - codecUserData->inputCond.notify_all(); - } - return 0; -} - -void AudioCapturer::AudioCapturerInit(SampleInfo &sampleInfo, CodecUserData *audioEncContext) -{ - AudioCapturerRelease(); - - // Create builder - OH_AudioStream_Type type = AUDIOSTREAM_TYPE_CAPTURER; - OH_AudioStreamBuilder_Create(&builder_, type); - // set params and callbacks - OH_AudioStreamBuilder_SetSamplingRate(builder_, sampleInfo.audioSampleRate); - OH_AudioStreamBuilder_SetChannelCount(builder_, sampleInfo.audioChannelCount); - OH_AudioStreamBuilder_SetSampleFormat(builder_, AUDIOSTREAM_SAMPLE_S16LE); - OH_AudioStreamBuilder_SetLatencyMode(builder_, AUDIOSTREAM_LATENCY_MODE_NORMAL); - OH_AudioStreamBuilder_SetEncodingType(builder_, AUDIOSTREAM_ENCODING_TYPE_RAW); - OH_AudioCapturer_Callbacks callbacks; - callbacks.OH_AudioCapturer_OnReadData = AudioCapturerOnReadData; - OH_AudioStreamBuilder_SetCapturerCallback(builder_, callbacks, audioEncContext); - // create OH_AudioCapturer - OH_AudioStreamBuilder_GenerateCapturer(builder_, &audioCapturer_); -} - -void AudioCapturer::AudioCapturerStart() -{ - if (audioCapturer_ != nullptr) { - OH_AudioCapturer_Start(audioCapturer_); - } -} - -void AudioCapturer::AudioCapturerRelease() -{ - if (audioCapturer_ != nullptr) { - OH_AudioCapturer_Stop(audioCapturer_); - OH_AudioCapturer_Release(audioCapturer_); - audioCapturer_ = nullptr; - } - if (builder_ != nullptr) { - OH_AudioStreamBuilder_Destroy(builder_); - builder_ = nullptr; - } -} \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/AudioDecoder.cpp b/entry/src/main/cpp/capbilities/AudioDecoder.cpp deleted file mode 100644 index b51bb78..0000000 --- a/entry/src/main/cpp/capbilities/AudioDecoder.cpp +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AudioDecoder.h" -#include "dfx/error/AVCodecSampleError.h" - -#undef LOG_TAG -#define LOG_TAG "AudioDecoder" - -AudioDecoder::~AudioDecoder() { Release(); } - -int32_t AudioDecoder::Create(const std::string &codecMime) { - decoder_ = OH_AudioCodec_CreateByMime(codecMime.c_str(), false); - CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed"); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioDecoder::SetCallback(CodecUserData *codecUserData) { - int32_t ret = AV_ERR_OK; - ret = OH_AudioCodec_RegisterCallback(decoder_, - {SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, - SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer}, - codecUserData); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret); - - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioDecoder::Configure(const SampleInfo &sampleInfo) { - OH_AVFormat *format = OH_AVFormat_Create(); - CHECK_AND_RETURN_RET_LOG(format != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "AVFormat create failed"); - - OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUDIO_SAMPLE_FORMAT, SAMPLE_S16LE); - OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_CHANNEL_COUNT, sampleInfo.audioChannelCount); - OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_SAMPLE_RATE, sampleInfo.audioSampleRate); - OH_AVFormat_SetLongValue(format, OH_MD_KEY_CHANNEL_LAYOUT, sampleInfo.audioChannelLayout); - if (sampleInfo.codecConfigLen > 0) { - AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ====== codecConfig:%{public}p, len:%{public}i, " - "adts:${public}i, 0:0x%{public}02x, 1:0x%{public}02x", - sampleInfo.codecConfig, sampleInfo.codecConfigLen, sampleInfo.aacAdts, - sampleInfo.codecConfig[0], sampleInfo.codecConfig[1]); - uint8_t tmpCodecConfig[2]; - tmpCodecConfig[0] = 0x13; // 0x11 - tmpCodecConfig[1] = 0x10; // 0x90 - tmpCodecConfig[0] = sampleInfo.codecConfig[0]; // 0x11 - tmpCodecConfig[1] = sampleInfo.codecConfig[1]; // 0x90 - AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ====== 0:0x%{public}02x, 1:0x%{public}02x", tmpCodecConfig[0], - tmpCodecConfig[1]); - OH_AVFormat_SetBuffer(format, OH_MD_KEY_CODEC_CONFIG, sampleInfo.codecConfig, sampleInfo.codecConfigLen); - } - - AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ======"); - int ret = OH_AudioCodec_Configure(decoder_, format); - AVCODEC_SAMPLE_LOGI("====== AudioDecoder config ======"); - OH_AVFormat_Destroy(format); - format = nullptr; - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Config failed, ret: %{public}d", ret); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioDecoder::Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData) { - CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); - CHECK_AND_RETURN_RET_LOG(codecUserData != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Invalid param: codecUserData"); - - // Configure audio decoder - int32_t ret = Configure(sampleInfo); - CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Configure failed"); - - // SetCallback for audio decoder - ret = SetCallback(codecUserData); - CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, - "Set callback failed, ret: %{public}d", ret); - - // Prepare audio decoder - { - int ret = OH_AudioCodec_Prepare(decoder_); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Prepare failed, ret: %{public}d", ret); - } - - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioDecoder::Start() { - CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); - - int ret = OH_AudioCodec_Start(decoder_); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioDecoder::PushInputBuffer(CodecBufferInfo &info) { - CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); - int32_t ret = OH_AVBuffer_SetBufferAttr(reinterpret_cast(info.buffer), &info.attr); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set avbuffer attr failed"); - ret = OH_AudioCodec_PushInputBuffer(decoder_, info.bufferIndex); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Push input data failed"); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioDecoder::FreeOutputBuffer(uint32_t bufferIndex, bool render) { - CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Decoder is null"); - - int32_t ret = AVCODEC_SAMPLE_ERR_OK; - ret = OH_AudioCodec_FreeOutputBuffer(decoder_, bufferIndex); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Free output data failed"); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioDecoder::Release() { - if (decoder_ != nullptr) { - OH_AudioCodec_Flush(decoder_); - OH_AudioCodec_Stop(decoder_); - OH_AudioCodec_Destroy(decoder_); - decoder_ = nullptr; - } - return AVCODEC_SAMPLE_ERR_OK; -} \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/AudioEncoder.cpp b/entry/src/main/cpp/capbilities/AudioEncoder.cpp deleted file mode 100644 index d6bf44b..0000000 --- a/entry/src/main/cpp/capbilities/AudioEncoder.cpp +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AudioEncoder.h" - -#undef LOG_TAG -#define LOG_TAG "AudioEncoder" - -namespace { -constexpr int LIMIT_LOGD_FREQUENCY = 50; -} // namespace - -AudioEncoder::~AudioEncoder() -{ - Release(); -} - -int32_t AudioEncoder::Create(const std::string &codecMime) -{ - encoder_ = OH_AudioCodec_CreateByMime(codecMime.c_str(), true); - CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed"); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::SetCallback(CodecUserData *codecUserData) -{ - int32_t ret = AV_ERR_OK; - ret = OH_AudioCodec_RegisterCallback(encoder_, - { SampleCallback::OnCodecError, SampleCallback::OnCodecFormatChange, - SampleCallback::OnNeedInputBuffer, SampleCallback::OnNewOutputBuffer }, - codecUserData); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set callback failed, ret: %{public}d", ret); - AVCODEC_SAMPLE_LOGI("====== AudioEncoder SetCallback ======"); - - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::Configure(const SampleInfo &sampleInfo) -{ - OH_AVFormat *format = OH_AVFormat_Create(); - CHECK_AND_RETURN_RET_LOG(format != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "AVFormat create failed"); - - OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUDIO_SAMPLE_FORMAT, sampleInfo.audioSampleForamt); - OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_CHANNEL_COUNT, sampleInfo.audioChannelCount); - OH_AVFormat_SetIntValue(format, OH_MD_KEY_AUD_SAMPLE_RATE, sampleInfo.audioSampleRate); - OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, sampleInfo.audioBitRate); - OH_AVFormat_SetLongValue(format, OH_MD_KEY_CHANNEL_LAYOUT, sampleInfo.audioChannelLayout); - OH_AVFormat_SetIntValue(format, OH_MD_KEY_MAX_INPUT_SIZE, sampleInfo.audioMaxInputSize); - AVCODEC_SAMPLE_LOGI("audioChannelCount:%{public}d audioSampleRate:%{public}d audioBitRate:%{public}d " - "audioChannelLayout:%{public}ld", - sampleInfo.audioChannelCount, sampleInfo.audioSampleRate, sampleInfo.audioBitRate, - sampleInfo.audioChannelLayout); - - int ret = OH_AudioCodec_Configure(encoder_, format); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Config failed, ret: %{public}d", ret); - OH_AVFormat_Destroy(format); - format = nullptr; - - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData) -{ - CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); - CHECK_AND_RETURN_RET_LOG(codecUserData != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Invalid param: codecUserData"); - - // Configure audio encoder - int32_t ret = Configure(sampleInfo); - CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Configure failed"); - - // SetCallback for audio encoder - ret = SetCallback(codecUserData); - CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, - "Set callback failed, ret: %{public}d", ret); - - // Prepare audio encoder - { - int ret = OH_AudioCodec_Prepare(encoder_); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Prepare failed, ret: %{public}d", ret); - } - - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::Start() -{ - CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); - - int ret = OH_AudioCodec_Start(encoder_); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Start failed, ret: %{public}d", ret); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::PushInputData(CodecBufferInfo &info) -{ - CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); - int32_t ret = OH_AVBuffer_SetBufferAttr(reinterpret_cast(info.buffer), &info.attr); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Set avbuffer attr failed"); - ret = OH_AudioCodec_PushInputBuffer(encoder_, info.bufferIndex); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Push input data failed"); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::FreeOutputData(uint32_t bufferIndex) -{ - CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); - - int32_t ret = AVCODEC_SAMPLE_ERR_OK; - ret = OH_AudioCodec_FreeOutputBuffer(encoder_, bufferIndex); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Free output data failed"); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::Stop() -{ - CHECK_AND_RETURN_RET_LOG(encoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Encoder is null"); - - int ret = OH_AudioCodec_Flush(encoder_); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Flush failed, ret: %{public}d", ret); - - ret = OH_AudioCodec_Stop(encoder_); - CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Stop failed, ret: %{public}d", ret); - return AVCODEC_SAMPLE_ERR_OK; -} - -int32_t AudioEncoder::Release() -{ - if (encoder_ != nullptr) { - OH_AudioCodec_Flush(encoder_); - OH_AudioCodec_Stop(encoder_); - OH_AudioCodec_Destroy(encoder_); - encoder_ = nullptr; - } - return AVCODEC_SAMPLE_ERR_OK; -} diff --git a/entry/src/main/cpp/capbilities/Muxer.cpp b/entry/src/main/cpp/capbilities/Muxer.cpp index 5d7fee6..167992f 100644 --- a/entry/src/main/cpp/capbilities/Muxer.cpp +++ b/entry/src/main/cpp/capbilities/Muxer.cpp @@ -21,7 +21,6 @@ #define LOG_TAG "Muxer" namespace { -constexpr int32_t CAMERA_ANGLE = 90; constexpr int32_t SAMPLE_RATE = 16000; } // namespace @@ -37,21 +36,13 @@ int32_t Muxer::Create(int32_t fd) { int32_t Muxer::Config(SampleInfo &sampleInfo) { CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer is null"); - -// OH_AVFormat *formatAudio = OH_AVFormat_CreateAudioFormat(sampleInfo.audioCodecMime.data(), -// sampleInfo.audioSampleRate, sampleInfo.audioChannelCount); -// CHECK_AND_RETURN_RET_LOG(formatAudio != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create audio format failed"); -// OH_AVFormat_SetIntValue(formatAudio, OH_MD_KEY_PROFILE, AAC_PROFILE_LC); -// int32_t ret = OH_AVMuxer_AddTrack(muxer_, &audioTrackId_, formatAudio); -// OH_AVFormat_Destroy(formatAudio); - OH_AVFormat *formatVideo = - OH_AVFormat_CreateVideoFormat(sampleInfo.outputVideoCodecMime.data(), sampleInfo.outputVideoWidth, sampleInfo.outputVideoHeight); + OH_AVFormat_CreateVideoFormat(sampleInfo.outputVideoCodecMime.data(), sampleInfo.videoWidth, sampleInfo.videoHeight); CHECK_AND_RETURN_RET_LOG(formatVideo != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create video format failed"); OH_AVFormat_SetDoubleValue(formatVideo, OH_MD_KEY_FRAME_RATE, sampleInfo.outputFrameRate); - OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_WIDTH, sampleInfo.outputVideoWidth); - OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_HEIGHT, sampleInfo.outputVideoHeight); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_WIDTH, sampleInfo.videoWidth); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_HEIGHT, sampleInfo.videoHeight); OH_AVFormat_SetStringValue(formatVideo, OH_MD_KEY_CODEC_MIME, sampleInfo.outputVideoCodecMime.data()); if (sampleInfo.isHDRVivid) { OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_VIDEO_IS_HDR_VIVID, 1); @@ -64,7 +55,7 @@ int32_t Muxer::Config(SampleInfo &sampleInfo) { int32_t ret = OH_AVMuxer_AddTrack(muxer_, &videoTrackId_, formatVideo); OH_AVFormat_Destroy(formatVideo); formatVideo = nullptr; - OH_AVMuxer_SetRotation(muxer_, CAMERA_ANGLE); + OH_AVMuxer_SetRotation(muxer_, sampleInfo.rotation); CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "AddTrack failed"); return AVCODEC_SAMPLE_ERR_OK; } diff --git a/entry/src/main/cpp/capbilities/VideoEncoder.cpp b/entry/src/main/cpp/capbilities/VideoEncoder.cpp index 3872366..bec3645 100644 --- a/entry/src/main/cpp/capbilities/VideoEncoder.cpp +++ b/entry/src/main/cpp/capbilities/VideoEncoder.cpp @@ -117,10 +117,10 @@ int32_t VideoEncoder::Configure(const SampleInfo &sampleInfo) { OH_AVFormat_SetIntValue(format, OH_MD_KEY_WIDTH, sampleInfo.videoWidth); OH_AVFormat_SetIntValue(format, OH_MD_KEY_HEIGHT, sampleInfo.videoHeight); - OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, sampleInfo.frameRate); + OH_AVFormat_SetDoubleValue(format, OH_MD_KEY_FRAME_RATE, sampleInfo.outputFrameRate); OH_AVFormat_SetIntValue(format, OH_MD_KEY_PIXEL_FORMAT, sampleInfo.pixelFormat); OH_AVFormat_SetIntValue(format, OH_MD_KEY_VIDEO_ENCODE_BITRATE_MODE, sampleInfo.bitrateMode); - OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, sampleInfo.bitrate); + OH_AVFormat_SetLongValue(format, OH_MD_KEY_BITRATE, sampleInfo.outputBitrate); OH_AVFormat_SetIntValue(format, OH_MD_KEY_PROFILE, sampleInfo.hevcProfile); // [EndExclude camera_AVCodec] // Setting HDRVivid-related parameters diff --git a/entry/src/main/cpp/capbilities/include/AudioCapturer.h b/entry/src/main/cpp/capbilities/include/AudioCapturer.h deleted file mode 100644 index d9e681a..0000000 --- a/entry/src/main/cpp/capbilities/include/AudioCapturer.h +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AVCODECVIDEO_AUDIOCAPTURER_H -#define AVCODECVIDEO_AUDIOCAPTURER_H - -#include -#include -#include - -#include "SampleInfo.h" - -class AudioCapturer { -public: - AudioCapturer() = default; - ~AudioCapturer(); - - void AudioCapturerInit(SampleInfo& sampleInfo, CodecUserData *audioEncContext); - void AudioCapturerStart(); - void AudioCapturerRelease(); - -private: - OH_AudioCapturer *audioCapturer_ = nullptr; - OH_AudioStreamBuilder *builder_ = nullptr; -}; - - -#endif //AVCODECVIDEO_AUDIOCAPTURER_H diff --git a/entry/src/main/cpp/capbilities/include/AudioDecoder.h b/entry/src/main/cpp/capbilities/include/AudioDecoder.h deleted file mode 100644 index 5c77684..0000000 --- a/entry/src/main/cpp/capbilities/include/AudioDecoder.h +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AUDIODECODER_H -#define AUDIODECODER_H - -#include "multimedia/player_framework/native_avcodec_audiocodec.h" -#include "multimedia/player_framework/native_avbuffer_info.h" -#include "SampleCallback.h" -#include "AVCodecSampleLog.h" - -class AudioDecoder { -public: - AudioDecoder() = default; - ~AudioDecoder(); - - int32_t Create(const std::string &codecMime); - int32_t Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData); - int32_t Start(); - int32_t PushInputBuffer(CodecBufferInfo &info); - int32_t FreeOutputBuffer(uint32_t bufferIndex, bool render); - int32_t Release(); - -private: - int32_t SetCallback(CodecUserData *codecUserData); - int32_t Configure(const SampleInfo &sampleInfo); - - bool isAVBufferMode_ = false; - OH_AVCodec *decoder_; -}; -#endif // AUDIODECODER_H \ No newline at end of file diff --git a/entry/src/main/cpp/capbilities/include/AudioEncoder.h b/entry/src/main/cpp/capbilities/include/AudioEncoder.h deleted file mode 100644 index 1261bf1..0000000 --- a/entry/src/main/cpp/capbilities/include/AudioEncoder.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AVCODECVIDEO_AUDIOENCODER_H -#define AVCODECVIDEO_AUDIOENCODER_H - -#include "multimedia/player_framework/native_avcodec_audiocodec.h" -#include "multimedia/player_framework/native_avbuffer_info.h" -#include "multimedia/native_audio_channel_layout.h" -#include "SampleInfo.h" -#include "SampleCallback.h" -#include "dfx/error/AVCodecSampleError.h" -#include "dfx/log/AVCodecSampleLog.h" - -class AudioEncoder { -public: - AudioEncoder() = default; - ~AudioEncoder(); - - int32_t Create(const std::string &codecMime); - int32_t Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData); - int32_t Start(); - int32_t PushInputData(CodecBufferInfo &info); - int32_t FreeOutputData(uint32_t bufferIndex); - int32_t Stop(); - int32_t Release(); - -private: - int32_t SetCallback(CodecUserData *codecUserData); - int32_t Configure(const SampleInfo &sampleInfo); - - bool isAVBufferMode_ = false; - OH_AVCodec *encoder_; -}; - -#endif // AVCODECVIDEO_AUDIOENCODER_H diff --git a/entry/src/main/cpp/common/SampleCallback.cpp b/entry/src/main/cpp/common/SampleCallback.cpp index b531c52..b90e5a2 100644 --- a/entry/src/main/cpp/common/SampleCallback.cpp +++ b/entry/src/main/cpp/common/SampleCallback.cpp @@ -21,58 +21,6 @@ namespace { constexpr int LIMIT_LOGD_FREQUENCY = 50; } -// Custom write data function -int32_t SampleCallback::OnRenderWriteData(OH_AudioRenderer *renderer, void *userData, void *buffer, int32_t length) { - (void)renderer; - (void)length; - CodecUserData *codecUserData = static_cast(userData); - - // Write the data to be played to the buffer by length - uint8_t *dest = (uint8_t *)buffer; - size_t index = 0; - std::unique_lock lock(codecUserData->outputMutex); - // Retrieve the length of the data to be played from the queue - while (!codecUserData->renderQueue.empty() && index < length) { - dest[index++] = codecUserData->renderQueue.front(); - codecUserData->renderQueue.pop(); - } - AVCODEC_SAMPLE_LOGD("render BufferLength:%{public}d Out buffer count: %{public}u, renderQueue.size: %{public}u " - "renderReadSize: %{public}u", - length, codecUserData->outputFrameCount, (unsigned int)codecUserData->renderQueue.size(), - (unsigned int)index); - if (codecUserData->renderQueue.size() < length) { - codecUserData->renderCond.notify_all(); - } - return 0; -} -// Customize the audio stream event function -int32_t SampleCallback::OnRenderStreamEvent(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Event event) { - (void)renderer; - (void)userData; - (void)event; - // Update the player status and interface based on the audio stream event information represented by the event - return 0; -} -// Customize the audio interrupt event function -int32_t SampleCallback::OnRenderInterruptEvent(OH_AudioRenderer *renderer, void *userData, - OH_AudioInterrupt_ForceType type, OH_AudioInterrupt_Hint hint) { - (void)renderer; - (void)userData; - (void)type; - (void)hint; - // Update the player status and interface based on the audio interrupt information indicated by type and hint - return 0; -} -// Custom exception callback functions -int32_t SampleCallback::OnRenderError(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Result error) { - (void)renderer; - (void)userData; - (void)error; - AVCODEC_SAMPLE_LOGE("OnRenderError"); - // Handle the audio exception information based on the error message - return 0; -} - void SampleCallback::OnCodecError(OH_AVCodec *codec, int32_t errorCode, void *userData) { (void)codec; (void)errorCode; diff --git a/entry/src/main/cpp/common/SampleCallback.h b/entry/src/main/cpp/common/SampleCallback.h index fa0736a..6f8c5be 100644 --- a/entry/src/main/cpp/common/SampleCallback.h +++ b/entry/src/main/cpp/common/SampleCallback.h @@ -15,19 +15,10 @@ #ifndef AVCODEC_SAMPLE_CALLBACK_H #define AVCODEC_SAMPLE_CALLBACK_H - -#include -#include #include "SampleInfo.h" class SampleCallback { public: - static int32_t OnRenderWriteData(OH_AudioRenderer *renderer, void *userData, void *buffer, int32_t length); - static int32_t OnRenderStreamEvent(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Event event); - static int32_t OnRenderInterruptEvent(OH_AudioRenderer *renderer, void *userData, OH_AudioInterrupt_ForceType type, - OH_AudioInterrupt_Hint hint); - static int32_t OnRenderError(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Result error); - static void OnCodecError(OH_AVCodec *codec, int32_t errorCode, void *userData); static void OnCodecFormatChange(OH_AVCodec *codec, OH_AVFormat *format, void *userData); static void OnNeedInputBuffer(OH_AVCodec *codec, uint32_t index, OH_AVBuffer *buffer, void *userData); diff --git a/entry/src/main/cpp/common/SampleInfo.h b/entry/src/main/cpp/common/SampleInfo.h index 7274246..72cad0d 100644 --- a/entry/src/main/cpp/common/SampleInfo.h +++ b/entry/src/main/cpp/common/SampleInfo.h @@ -44,8 +44,6 @@ struct SampleInfo { int32_t videoWidth = 0; int32_t videoHeight = 0; - int32_t outputVideoWidth = 0; - int32_t outputVideoHeight = 0; double outputFrameRate = 0.0; std::string outputVideoCodecMime = ""; int64_t outputBitrate = 10 * 1024 * 1024; // 10Mbps; @@ -113,50 +111,15 @@ public: uint32_t outputFrameCount = 0; std::mutex outputMutex; std::condition_variable outputCond; - std::mutex renderMutex; std::condition_variable renderCond; std::queue outputBufferInfoQueue; - std::queue renderQueue; - int32_t width = 0; int32_t height = 0; int32_t widthStride = 0; int32_t heightStride = 0; bool isEncFirstFrame = true; bool isDecFirstFrame = true; - - // Create cache - std::vector cache; - int32_t remainlen = 0; - - void ClearCache() - { - cache.clear(); - remainlen = 0; - } - - void WriteCache(void *buffer, int32_t bufferLen) - { - if (bufferLen + remainlen > cache.size()) { - cache.resize(remainlen + bufferLen); - } - std::memcpy(cache.data() + remainlen, buffer, bufferLen); - remainlen += bufferLen; - } - - bool ReadCache(void *buffer, int32_t bufferLen) - { - if (remainlen < bufferLen) { - return false; - } - std::memcpy(buffer, cache.data(), bufferLen); - remainlen = remainlen - bufferLen; - if (remainlen > 0) { - std::memmove(cache.data(), cache.data() + bufferLen, remainlen); - } - return true; - } }; #endif // AVCODEC_SAMPLE_INFO_H \ No newline at end of file diff --git a/entry/src/main/cpp/sample/transcoding/Transcoding.cpp b/entry/src/main/cpp/sample/transcoding/Transcoding.cpp index 0921078..b4c1768 100644 --- a/entry/src/main/cpp/sample/transcoding/Transcoding.cpp +++ b/entry/src/main/cpp/sample/transcoding/Transcoding.cpp @@ -28,13 +28,7 @@ using namespace std::chrono_literals; constexpr int8_t YUV420_SAMPLE_RATIO = 2; } // namespace -Transcoding::~Transcoding() { Transcoding::StartDecRelease(); } - -int32_t Transcoding::CreateAudioDecoder() { - AVCODEC_SAMPLE_LOGW("audio mime:%{public}s", sampleInfo_.audioCodecMime.c_str()); - int32_t ret = audioDecoder_->Create(sampleInfo_.audioCodecMime); - return ret; -} +Transcoding::~Transcoding() { Transcoding::StartRelease(); } int32_t Transcoding::CreateVideoDecoder() { AVCODEC_SAMPLE_LOGW("video mime:%{public}s", sampleInfo_.videoCodecMime.c_str()); @@ -52,7 +46,7 @@ int32_t Transcoding::CreateVideoDecoder() { int32_t Transcoding::Init(SampleInfo &sampleInfo) { std::unique_lock lock(mutex_); CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); - CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr && audioDecoder_ == nullptr, + CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); sampleInfo_ = sampleInfo; @@ -68,7 +62,7 @@ int32_t Transcoding::Init(SampleInfo &sampleInfo) { AVCODEC_SAMPLE_LOGE("Create video encoder failed"); doneCond_.notify_all(); lock.unlock(); - StartDecRelease(); + StartRelease(); return AVCODEC_SAMPLE_ERR_ERROR; } @@ -78,20 +72,14 @@ int32_t Transcoding::Init(SampleInfo &sampleInfo) { int32_t Transcoding::InitDecoder() { CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); - CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr && audioDecoder_ == nullptr, + CHECK_AND_RETURN_RET_LOG(demuxer_ == nullptr && videoDecoder_ == nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); videoDecoder_ = std::make_unique(); - audioDecoder_ = std::make_unique(); demuxer_ = std::make_unique(); isReleased_ = false; int32_t ret = demuxer_->Create(sampleInfo_); -// if (ret == AVCODEC_SAMPLE_ERR_OK) { -// ret = CreateAudioDecoder(); -// } else { -// AVCODEC_SAMPLE_LOGE("Create demuxer failed"); -// } if (ret == AVCODEC_SAMPLE_ERR_OK) { ret = CreateVideoDecoder(); @@ -101,21 +89,8 @@ int32_t Transcoding::InitDecoder() { return ret; } -int32_t Transcoding::CreateAudioEncoder() { - int32_t ret = audioEncoder_->Create(sampleInfo_.audioCodecMime); - CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create audio encoder(%{public}s) failed", - sampleInfo_.audioCodecMime.c_str()); - AVCODEC_SAMPLE_LOGI("Create audio encoder(%{public}s)", sampleInfo_.audioCodecMime.c_str()); - - audioEncContext_ = new CodecUserData; - ret = audioEncoder_->Config(sampleInfo_, audioEncContext_); - CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder config failed"); - - return AVCODEC_SAMPLE_ERR_OK; -} - int32_t Transcoding::CreateVideoEncoder() { - int32_t ret = videoEncoder_->Create(sampleInfo_.videoCodecMime); + int32_t ret = videoEncoder_->Create(sampleInfo_.outputVideoCodecMime); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed"); videoEncContext_ = new CodecUserData; @@ -127,27 +102,22 @@ int32_t Transcoding::CreateVideoEncoder() { int32_t Transcoding::InitEncoder() { CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); - CHECK_AND_RETURN_RET_LOG(muxer_ == nullptr && videoEncoder_ == nullptr && audioEncoder_ == nullptr, + CHECK_AND_RETURN_RET_LOG(muxer_ == nullptr && videoEncoder_ == nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); - - audioEncoder_ = std::make_unique(); + videoEncoder_ = std::make_unique(); muxer_ = std::make_unique(); - - int32_t ret = videoEncoder_->Create(sampleInfo_.outputVideoCodecMime); - CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed"); - ret = muxer_->Create(sampleInfo_.outputFd); + + int32_t ret = muxer_->Create(sampleInfo_.outputFd); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create muxer with fd(%{public}d) failed", sampleInfo_.outputFd); ret = muxer_->Config(sampleInfo_); - -// ret = CreateAudioEncoder(); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create audio encoder failed"); ret = CreateVideoEncoder(); CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Create video encoder failed"); - -// releaseThread_ = nullptr; + AVCODEC_SAMPLE_LOGI("Succeed"); return AVCODEC_SAMPLE_ERR_OK; } @@ -163,7 +133,7 @@ int32_t Transcoding::Start() { if (ret != AVCODEC_SAMPLE_ERR_OK) { AVCODEC_SAMPLE_LOGE("Video Decoder start failed"); lock.unlock(); - StartDecRelease(); + StartRelease(); return AVCODEC_SAMPLE_ERR_ERROR; } isStarted_ = true; @@ -173,7 +143,7 @@ int32_t Transcoding::Start() { if (videoDecInputThread_ == nullptr || videoDecOutputThread_ == nullptr) { AVCODEC_SAMPLE_LOGE("Create thread failed"); lock.unlock(); - StartDecRelease(); + StartRelease(); return AVCODEC_SAMPLE_ERR_ERROR; } } @@ -188,59 +158,31 @@ int32_t Transcoding::Start() { videoEncOutputThread_ = std::make_unique(&Transcoding::VideoEncOutputThread, this); if (videoEncOutputThread_ == nullptr) { AVCODEC_SAMPLE_LOGE("Create thread failed"); - StartEncRelease(); + StartRelease(); return AVCODEC_SAMPLE_ERR_ERROR; } } -// if (audioDecContext_) { -// ret = audioDecoder_->Start(); -// if (ret != AVCODEC_SAMPLE_ERR_OK) { -// AVCODEC_SAMPLE_LOGE("Audio Decoder start failed"); -// lock.unlock(); -// StartRelease(); -// return AVCODEC_SAMPLE_ERR_ERROR; -// } -// isStarted_ = true; -// audioDecInputThread_ = std::make_unique(&Transcoding::AudioDecInputThread, this); -// audioDecOutputThread_ = std::make_unique(&Transcoding::AudioDecOutputThread, this); -// if (audioDecInputThread_ == nullptr || audioDecOutputThread_ == nullptr) { -// AVCODEC_SAMPLE_LOGE("Create thread failed"); -// lock.unlock(); -// StartRelease(); -// return AVCODEC_SAMPLE_ERR_ERROR; -// } -// } - // Clear the queue -// while (audioDecContext_ && !audioDecContext_->renderQueue.empty()) { -// audioDecContext_->renderQueue.pop(); -// } - AVCODEC_SAMPLE_LOGI("Succeed"); doneCond_.notify_all(); return AVCODEC_SAMPLE_ERR_OK; } void Transcoding::Stop() { - if (!isReleased_) { - isReleased_ = true; - DecRelease(); - } - StartEncRelease(); + StartRelease(); } -void Transcoding::StartDecRelease() { - AVCODEC_SAMPLE_LOGI("start release"); - std::unique_lock lock(doneMutex); - doneCond_.wait(lock, [this]() { return isAudioDone.load(); }); - +void Transcoding::StartRelease() { if (!isReleased_) { isReleased_ = true; - DecRelease(); + Release(); } + AVCODEC_SAMPLE_LOGI("StartRelease Done"); } -void Transcoding::ReleaseThread() { +void Transcoding::Release() { + std::lock_guard lock(mutex_); + isStarted_ = false; if (videoDecInputThread_ && videoDecInputThread_->joinable()) { videoDecInputThread_->detach(); videoDecInputThread_.reset(); @@ -249,20 +191,15 @@ void Transcoding::ReleaseThread() { videoDecOutputThread_->detach(); videoDecOutputThread_.reset(); } - if (audioDecInputThread_ && audioDecInputThread_->joinable()) { - audioDecInputThread_->detach(); - audioDecInputThread_.reset(); + if (videoEncOutputThread_ && videoEncOutputThread_->joinable()) { + videoEncOutputThread_->detach(); + videoEncOutputThread_.reset(); } - if (audioDecOutputThread_ && audioDecOutputThread_->joinable()) { - audioDecOutputThread_->detach(); - audioDecOutputThread_.reset(); + if (muxer_ != nullptr) { + muxer_->Release(); + muxer_.reset(); + AVCODEC_SAMPLE_LOGI("Muxer release successful"); } -} - -void Transcoding::DecRelease() { - std::lock_guard lock(mutex_); - ReleaseThread(); - if (demuxer_ != nullptr) { demuxer_->Release(); demuxer_.reset(); @@ -271,100 +208,32 @@ void Transcoding::DecRelease() { videoDecoder_->Release(); videoDecoder_.reset(); } - if (videoDecContext_ != nullptr) { - delete videoDecContext_; - videoDecContext_ = nullptr; - } - if (audioDecoder_ != nullptr) { - audioDecoder_->Release(); - audioDecoder_.reset(); - } - if (audioDecContext_ != nullptr) { - delete audioDecContext_; - audioDecContext_ = nullptr; - } - OH_AudioStreamBuilder_Destroy(builder_); - builder_ = nullptr; - doneCond_.notify_all(); - // Trigger the callback - sampleInfo_.playDoneCallback(sampleInfo_.playDoneCallbackData); - // Clear the queue - while (audioDecContext_ && !audioDecContext_->renderQueue.empty()) { - audioDecContext_->renderQueue.pop(); - } - AVCODEC_SAMPLE_LOGI("Succeed"); -} - -void Transcoding::StartEncRelease() { - if (releaseEncThread_ == nullptr) { - AVCODEC_SAMPLE_LOGI("Start release CodecTest"); - releaseEncThread_ = std::make_unique(&Transcoding::EncRelease, this); - } -} - -void Transcoding::EncRelease() { - std::lock_guard lock(mutex_); - isStarted_ = false; - if (videoEncOutputThread_ && videoEncOutputThread_->joinable()) { - videoEncOutputThread_->join(); - videoEncOutputThread_.reset(); - } - if (audioEncInputThread_ && audioEncInputThread_->joinable()) { - audioEncContext_->inputCond.notify_all(); - audioEncInputThread_->join(); - audioEncInputThread_.reset(); - } - if (audioEncOutputThread_ && audioEncOutputThread_->joinable()) { - audioEncContext_->outputCond.notify_all(); - audioEncOutputThread_->join(); - audioEncOutputThread_.reset(); - } - if (muxer_ != nullptr) { - muxer_->Release(); - muxer_.reset(); - AVCODEC_SAMPLE_LOGI("Muxer release successful"); - } if (videoEncoder_ != nullptr) { videoEncoder_->Stop(); - if (sampleInfo_.window != nullptr) { - OH_NativeWindow_DestroyNativeWindow(sampleInfo_.window); - sampleInfo_.window = nullptr; - } videoEncoder_->Release(); videoEncoder_.reset(); AVCODEC_SAMPLE_LOGI("Video encoder release successful"); } - if (audioEncoder_ != nullptr) { - audioEncoder_->Stop(); - audioEncoder_->Release(); - audioEncoder_.reset(); - AVCODEC_SAMPLE_LOGI("Audio encoder release successful"); - } - if (audioEncContext_ != nullptr) { - delete audioEncContext_; - audioEncContext_ = nullptr; - } if (videoEncContext_ != nullptr) { delete videoEncContext_; videoEncContext_ = nullptr; } + if (videoDecContext_ != nullptr) { + delete videoDecContext_; + videoDecContext_ = nullptr; + } doneCond_.notify_all(); - AVCODEC_SAMPLE_LOGI("Succeed"); + sampleInfo_.playDoneCallback(sampleInfo_.playDoneCallbackData); + AVCODEC_SAMPLE_LOGI("Release Succeed"); } void Transcoding::CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBufferInfo &bufferInfo) { - auto &info = sampleInfo_; - int32_t videoWidth = - videoDecContext_->width * - ((info.videoCodecMime == OH_AVCODEC_MIMETYPE_VIDEO_HEVC && info.hevcProfile == HEVC_PROFILE_MAIN_10) ? 2 : 1); + int32_t videoWidth = videoDecContext_->width; int32_t &stride = videoDecContext_->widthStride; - int32_t uvWidth = videoWidth / YUV420_SAMPLE_RATIO; - int32_t uvStride = stride / YUV420_SAMPLE_RATIO; int32_t size = 0; uint8_t *tempBufferAddr = encBufferInfo.bufferAddr; - size += videoDecContext_->height * videoWidth; - size += videoDecContext_->height * uvWidth; + size += videoDecContext_->height * videoWidth * 3 / 2; if (videoWidth == videoDecContext_->widthStride && videoDecContext_->heightStride == videoDecContext_->height) { std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, size); } else { @@ -376,20 +245,11 @@ void Transcoding::CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBuffer } bufferInfo.bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) * stride; - // copy U - for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { - std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); - tempBufferAddr += uvWidth; - bufferInfo.bufferAddr += uvStride; - } - bufferInfo.bufferAddr += - (videoDecContext_->heightStride - videoDecContext_->height) / YUV420_SAMPLE_RATIO * uvStride; - - // copy V - for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { - std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, uvWidth); - tempBufferAddr += uvWidth; - bufferInfo.bufferAddr += uvStride; + // copy U/V + for (int32_t row = 0; row < (videoDecContext_->height / 2); row++) { + std::memcpy(tempBufferAddr, bufferInfo.bufferAddr, videoWidth); + tempBufferAddr += videoWidth; + bufferInfo.bufferAddr += stride; } } @@ -429,7 +289,6 @@ void Transcoding::VideoDecInputThread() { } void Transcoding::VideoDecOutputThread() { - isVideoDone = false; sampleInfo_.frameInterval = MICROSECOND / sampleInfo_.frameRate; while (true) { CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); @@ -465,6 +324,7 @@ void Transcoding::VideoDecOutputThread() { encBufferInfo.bufferAddr = OH_AVBuffer_GetAddr(reinterpret_cast(encBufferInfo.buffer)); bufferInfo.bufferAddr = OH_AVBuffer_GetAddr(reinterpret_cast(bufferInfo.buffer)); CopyStrideYUV420SP(encBufferInfo, bufferInfo); +// CopyStrideRGBA(encBufferInfo, bufferInfo); AVCODEC_SAMPLE_LOGW( "Out encBufferInfo flags: %{public}u, offset: %{public}d, pts: %{public}u, size: %{public}d" PRId64, @@ -484,8 +344,6 @@ void Transcoding::VideoDecOutputThread() { break; } } - isVideoDone = true; - StartDecRelease(); } void Transcoding::VideoEncOutputThread() { @@ -519,72 +377,5 @@ void Transcoding::VideoEncOutputThread() { CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Encoder output thread out"); } AVCODEC_SAMPLE_LOGI("Exit, frame count: %{public}u", videoEncContext_->outputFrameCount); - StartEncRelease(); -} - - -void Transcoding::AudioDecInputThread() { - while (true) { - CHECK_AND_BREAK_LOG(isStarted_, "Decoder input thread out"); - std::unique_lock lock(audioDecContext_->inputMutex); - bool condRet = audioDecContext_->inputCond.wait_for( - lock, 5s, [this]() { return !isStarted_ || !audioDecContext_->inputBufferInfoQueue.empty(); }); - CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out"); - CHECK_AND_CONTINUE_LOG(!audioDecContext_->inputBufferInfoQueue.empty(), - "Buffer queue is empty, continue, cond ret: %{public}d", condRet); - - CodecBufferInfo bufferInfo = audioDecContext_->inputBufferInfoQueue.front(); - audioDecContext_->inputBufferInfoQueue.pop(); - audioDecContext_->inputFrameCount++; - lock.unlock(); - - demuxer_->ReadSample(demuxer_->GetAudioTrackId(), reinterpret_cast(bufferInfo.buffer), - bufferInfo.attr); - - int32_t ret = audioDecoder_->PushInputBuffer(bufferInfo); - CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Push data failed, thread out"); - - CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out"); - } -} - -void Transcoding::AudioDecOutputThread() { - isAudioDone = false; - while (true) { - CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); - std::unique_lock lock(audioDecContext_->outputMutex); - bool condRet = audioDecContext_->outputCond.wait_for( - lock, 5s, [this]() { return !isStarted_ || !audioDecContext_->outputBufferInfoQueue.empty(); }); - CHECK_AND_BREAK_LOG(isStarted_, "Decoder output thread out"); - CHECK_AND_CONTINUE_LOG(!audioDecContext_->outputBufferInfoQueue.empty(), - "Buffer queue is empty, continue, cond ret: %{public}d", condRet); - - CodecBufferInfo bufferInfo = audioDecContext_->outputBufferInfoQueue.front(); - audioDecContext_->outputBufferInfoQueue.pop(); - CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out"); - audioDecContext_->outputFrameCount++; - AVCODEC_SAMPLE_LOGW("Out buffer count: %{public}u, size: %{public}d, flag: %{public}u, pts: %{public}" PRId64, - audioDecContext_->outputFrameCount, bufferInfo.attr.size, bufferInfo.attr.flags, - bufferInfo.attr.pts); - uint8_t *source = OH_AVBuffer_GetAddr(reinterpret_cast(bufferInfo.buffer)); - // Put the decoded PMC data into the queue - for (int i = 0; i < bufferInfo.attr.size; i++) { - audioDecContext_->renderQueue.push(*(source + i)); - } - lock.unlock(); - - int32_t ret = audioDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, true); - CHECK_AND_BREAK_LOG(ret == AVCODEC_SAMPLE_ERR_OK, "Decoder output thread out"); - - std::unique_lock lockRender(audioDecContext_->renderMutex); - audioDecContext_->renderCond.wait_for(lockRender, 20ms, [this, bufferInfo]() { - return audioDecContext_->renderQueue.size() < BALANCE_VALUE * bufferInfo.attr.size; - }); - } - std::unique_lock lockRender(audioDecContext_->renderMutex); - audioDecContext_->renderCond.wait_for(lockRender, 500ms, - [this]() { return audioDecContext_->renderQueue.size() < 1; }); - isAudioDone = true; - AVCODEC_SAMPLE_LOGI("Out buffer end"); - StartDecRelease(); + StartRelease(); } \ No newline at end of file diff --git a/entry/src/main/cpp/sample/transcoding/Transcoding.h b/entry/src/main/cpp/sample/transcoding/Transcoding.h index 81be6e0..d24a8f8 100644 --- a/entry/src/main/cpp/sample/transcoding/Transcoding.h +++ b/entry/src/main/cpp/sample/transcoding/Transcoding.h @@ -26,12 +26,10 @@ #include #include #include "VideoDecoder.h" -#include "AudioDecoder.h" #include "multimedia/player_framework/native_avbuffer.h" #include "Demuxer.h" #include "SampleInfo.h" #include "VideoEncoder.h" -#include "AudioEncoder.h" #include "Muxer.h" class Transcoding { @@ -54,25 +52,16 @@ private: void VideoDecInputThread(); void VideoDecOutputThread(); void VideoEncOutputThread(); - void AudioDecInputThread(); - void AudioDecOutputThread(); - void DecRelease(); void CopyStrideYUV420SP(CodecBufferInfo &encBufferInfo, CodecBufferInfo &bufferInfo); - void StartDecRelease(); - void EncRelease(); - void StartEncRelease(); - void ReleaseThread(); - int32_t CreateAudioDecoder(); + void Release(); + void StartRelease(); int32_t CreateVideoDecoder(); - int32_t CreateAudioEncoder(); int32_t CreateVideoEncoder(); std::unique_ptr videoDecoder_ = nullptr; - std::shared_ptr audioDecoder_ = nullptr; std::unique_ptr demuxer_ = nullptr; std::unique_ptr videoEncoder_ = nullptr; - std::unique_ptr audioEncoder_ = nullptr; std::unique_ptr muxer_ = nullptr; std::mutex mutex_; @@ -89,7 +78,6 @@ private: std::unique_ptr videoEncOutputThread_ = nullptr; std::unique_ptr audioEncInputThread_ = nullptr; std::unique_ptr audioEncOutputThread_ = nullptr; - std::unique_ptr releaseEncThread_ = nullptr; std::condition_variable doneCond_; SampleInfo sampleInfo_; CodecUserData *videoDecContext_ = nullptr; diff --git a/entry/src/main/cpp/sample/transcoding/TranscodingNative.cpp b/entry/src/main/cpp/sample/transcoding/TranscodingNative.cpp index 124c0d8..0be4ef4 100644 --- a/entry/src/main/cpp/sample/transcoding/TranscodingNative.cpp +++ b/entry/src/main/cpp/sample/transcoding/TranscodingNative.cpp @@ -14,7 +14,8 @@ */ #include "TranscodingNative.h" - +#include +#include "dfx/error/AVCodecSampleError.h" #undef LOG_DOMAIN #undef LOG_TAG #define LOG_DOMAIN 0xFF00 @@ -50,8 +51,8 @@ void Callback(void *asyncContext) { napi_value TranscodingNative::Start(napi_env env, napi_callback_info info) { SampleInfo sampleInfo; - size_t argc = 10; - napi_value args[10] = {nullptr}; + size_t argc = 8; + napi_value args[8] = {nullptr}; napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); napi_get_value_int32(env, args[0], &sampleInfo.inputFd); @@ -63,14 +64,12 @@ napi_value TranscodingNative::Start(napi_env env, napi_callback_info info) { size_t len = 20; napi_get_value_string_utf8(env, args[4], videoCodecMime, len, &videoCodecMimeStrlen); sampleInfo.outputVideoCodecMime = videoCodecMime; - napi_get_value_int32(env, args[5], &sampleInfo.outputVideoWidth); - napi_get_value_int32(env, args[6], &sampleInfo.outputVideoHeight); - napi_get_value_double(env, args[7], &sampleInfo.outputFrameRate); - napi_get_value_int64(env, args[8], &sampleInfo.outputBitrate); + napi_get_value_double(env, args[5], &sampleInfo.outputFrameRate); + napi_get_value_int64(env, args[6], &sampleInfo.outputBitrate); auto asyncContext = new CallbackContext(); asyncContext->env = env; - napi_create_reference(env, args[9], 1, &asyncContext->callbackRef); + napi_create_reference(env, args[7], 1, &asyncContext->callbackRef); sampleInfo.playDoneCallback = &Callback; sampleInfo.playDoneCallbackData = asyncContext; @@ -98,7 +97,7 @@ static napi_value Init(napi_env env, napi_value exports) { } EXTERN_C_END -static napi_module PlayerModule = { +static napi_module TranscodingModule = { .nm_version = 1, .nm_flags = 0, .nm_filename = nullptr, @@ -108,4 +107,4 @@ static napi_module PlayerModule = { .reserved = {0}, }; -extern "C" __attribute__((constructor)) void RegisterPlayerModule(void) { napi_module_register(&PlayerModule); } \ No newline at end of file +extern "C" __attribute__((constructor)) void RegisterPlayerModule(void) { napi_module_register(&TranscodingModule); } \ No newline at end of file diff --git a/entry/src/main/cpp/sample/transcoding/TranscodingNative.h b/entry/src/main/cpp/sample/transcoding/TranscodingNative.h index 8178e3f..c854e2b 100644 --- a/entry/src/main/cpp/sample/transcoding/TranscodingNative.h +++ b/entry/src/main/cpp/sample/transcoding/TranscodingNative.h @@ -15,15 +15,10 @@ #ifndef VIDEO_CODEC_SAMPLE_PLAYER_NATIVE_H #define VIDEO_CODEC_SAMPLE_PLAYER_NATIVE_H - #include #include -#include -#include #include "napi/native_api.h" #include "Transcoding.h" -#include "dfx/error/AVCodecSampleError.h" -#include "AVCodecSampleLog.h" class TranscodingNative { public: diff --git a/entry/src/main/cpp/types/libtranscoding/index.d.ts b/entry/src/main/cpp/types/libtranscoding/index.d.ts index d826257..63a1e77 100644 --- a/entry/src/main/cpp/types/libtranscoding/index.d.ts +++ b/entry/src/main/cpp/types/libtranscoding/index.d.ts @@ -19,8 +19,6 @@ export const startNative: ( inputFileOffset: number, inputFileSize: number, videoCodecMime: string, - width: number, - height: number, frameRate: number, bitRate: number, cbFn: () => void diff --git a/entry/src/main/ets/common/CommonConstants.ets b/entry/src/main/ets/common/CommonConstants.ets index d6a6601..0b8ef4e 100644 --- a/entry/src/main/ets/common/CommonConstants.ets +++ b/entry/src/main/ets/common/CommonConstants.ets @@ -133,7 +133,7 @@ export class CommonConstants { /** * Video resolution. */ - static readonly VIDEO_RESOLUTION: string[] = ['4K', '1080P', '720P']; + static readonly VIDEO_RESOLUTION: string[] = ['30M', '20M', '10M']; /** * Video framerate. */ diff --git a/entry/src/main/ets/model/VideoDataModel.ets b/entry/src/main/ets/model/VideoDataModel.ets index d613862..dca2f01 100644 --- a/entry/src/main/ets/model/VideoDataModel.ets +++ b/entry/src/main/ets/model/VideoDataModel.ets @@ -18,8 +18,6 @@ import { CommonConstants as Const } from '../common/CommonConstants'; export class VideoDataModel { public surfaceId: string = ''; - public width: number = Const.DEFAULT_WIDTH; - public height: number = Const.DEFAULT_HEIGHT; public isHDRVivid: number = Const.DEFAULT_VALUE; public outputfd: number = -1; public frameRate: number = Const.FRAMERATE_VIDEO_30FPS; @@ -32,9 +30,7 @@ export class VideoDataModel { this.videoCodecMime = codecMime; } - setResolution(width: number, height: number, bit: number): void { - this.width = width; - this.height = height; + setResolution(bit: number): void { this.bitRate = bit; } } \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 5bda9ae..b842f3a 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -13,30 +13,36 @@ * limitations under the License. */ -import { photoAccessHelper } from '@kit.MediaLibraryKit'; -import Logger from '../common/utils/Logger'; import { CommonConstants as Const } from '../common/CommonConstants'; -import { VideoTranscoding } from './VideoTranscoding'; import { VideoPlayer } from './VideoPlayer'; - -const TAG: string = Const.INDEX_TAG; +import { LoadingDialog } from '@kit.ArkUI'; +import { VideoDataModel } from '../model/VideoDataModel'; +import { fileIo } from '@kit.CoreFileKit'; +import transcoding from 'libtranscoding.so'; +import { formatVideoTime } from '../common/utils/TimeUtils'; @Entry @Component struct Index { @Provide('NavPathStack') pageStack: NavPathStack = new NavPathStack(); - @State selectFilePath: string | null = null; + @State currentTime: number = 0; + @State durationTime: number = 0; + @State isStart: boolean = true; + @State isTranscoding: boolean = false; + isConfig: boolean = false; + private videoDataModel: VideoDataModel = new VideoDataModel(); + private controller: VideoController = new VideoController(); + customDialogId: number = 0; + dialogController: CustomDialogController = new CustomDialogController({ + builder: LoadingDialog({ + content: $r('app.string.transcoding') + }), + autoCancel: false + }); @Builder PagesMap(name: string) { - if (name === 'VideoTranscoding') { - NavDestination() { - VideoTranscoding() - } - .title($r('app.string.video_transcoding')) - .backgroundColor('#F1F3F5') - .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) - } else if (name === 'VideoPlayer') { + if (name === 'VideoPlayer') { NavDestination() { VideoPlayer() } @@ -52,28 +58,164 @@ struct Index { Text($r('app.string.buffer_mode')) .width('100%') .fontColor('#E6000000') - .fontSize(30) + .fontSize(26) .fontWeight(700) .lineHeight(40) .textAlign(TextAlign.Start) - .margin({ top: 64 }) + .padding(16) + + Blank() + + Column() { + Stack() { + Video({ src: $rawfile('video_sample.mp4'), controller: this.controller }) + .width('100%') + .height('100%') + .autoPlay(true) + .controls(false) + .objectFit(1) + .zIndex(0) + .onPrepared((event) => { + if (event) { + this.durationTime = event.duration + console.info(this.durationTime.toString()); + } + }) + .onUpdate((event) => { + if (event) { + this.currentTime = event.time + } + }) + .onFinish(() => { + this.isStart = !this.isStart; + }) + .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) + + Row() { + Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) + .width(18) + .height(18) + .onClick(() => { + if (this.isStart) { + this.controller.pause(); + this.isStart = !this.isStart; + } else { + this.controller.start(); + this.isStart = !this.isStart; + } + }) + + Text(formatVideoTime(this.currentTime)) + .fontColor(Color.White) + .fontSize(12) + .margin({ left: '12vp' }) + Slider({ + value: this.currentTime, + min: 0, + max: this.durationTime + }) + .onChange((value: number, mode: SliderChangeMode) => { + // Set the video playback progress to jump to the value + this.controller.setCurrentTime(value); + }) + .layoutWeight(1) + Text(formatVideoTime(this.durationTime)) + .fontColor(Color.White) + .fontSize(12) + } + .padding({ left: '16vp', right: '16vp' }) + .zIndex(1) + .height(43) + .width('100%') + } + .alignContent(Alignment.Bottom) + .width('100%') + .layoutWeight(1) + } + .backgroundColor('') + .width('100%') + .layoutWeight(1) Blank() - Button($r('app.string.select_video')) - .onClick(() => { - this.selectFile(); - }) - .size({ - width: '100%', - height: 40 - }) + Column() { + Button($r('app.string.config_video')) + .id('config') + .onClick(() => { + this.getUIContext().showTextPickerDialog({ + defaultPickerItemHeight: Const.DEFAULT_PICKER_ITEM_HEIGHT, + selectedTextStyle: ({ + font: ({ + size: Const.SELECTED_TEXT_STYLE_FONT_SIZE + }) + }), + range: Const.RECORDER_INFO, + canLoop: false, + alignment: DialogAlignment.Center, + onAccept: (value: TextPickerResult) => { + switch (value.value[0]) { + case Const.VIDEO_MIMETYPE[0]: { + this.videoDataModel.setCodecFormat(Const.TRUE, Const.MIME_VIDEO_AVC); + break; + } + case Const.VIDEO_MIMETYPE[1]: { + this.videoDataModel.setCodecFormat(Const.FALSE, Const.MIME_VIDEO_HEVC); + break; + } + default: + break; + } + + switch (value.value[1]) { + case Const.VIDEO_RESOLUTION[0]: { + this.videoDataModel.setResolution(Const.BITRATE_VIDEO_30M); + break; + } + case Const.VIDEO_RESOLUTION[1]: { + this.videoDataModel.setResolution(Const.BITRATE_VIDEO_20M); + break; + } + case Const.VIDEO_RESOLUTION[2]: { + this.videoDataModel.setResolution(Const.BITRATE_VIDEO_10M); + break; + } + default: + break; + } + + switch (value.value[2]) { + case Const.VIDEO_FRAMERATE[0]: { + this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_30FPS; + break; + } + case Const.VIDEO_FRAMERATE[1]: { + this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_60FPS; + break; + } + default: + break; + } + } + }); + + }) + .size({ + width: '100%', + height: 40 + }) + .margin({ + bottom: 16 + }) + + Button($r('app.string.start_transcoding')) + .onClick(() => { + this.dialogController.open(); + this.transcoding(); + }) + .width('100%') + } + .padding(16) } - .padding({ - left: 16, - right: 16, - bottom: 16 - }) .width('100%') .height('100%') .justifyContent(FlexAlign.Start) @@ -85,25 +227,18 @@ struct Index { .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM]) } - selectFile() { - let photoPicker = new photoAccessHelper.PhotoViewPicker(); - photoPicker.select({ - MIMEType: photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE, - maxSelectNumber: 1 - }) - .then((photoSelectResult) => { - this.selectFilePath = photoSelectResult.photoUris[0]; - if (this.selectFilePath === null || this.selectFilePath === undefined) { - this.getUIContext().getPromptAction().showToast({ - message: $r('app.string.alert'), - duration: Const.DURATION, - bottom: Const.BOTTOM - }); - } else { - AppStorage.setOrCreate('selectedFile', this.selectFilePath); - this.pageStack.pushPathByName('VideoTranscoding',''); - Logger.info(TAG, 'documentViewPicker.select to file succeed and URI is:' + this.selectFilePath); - } - }); + transcoding() { + this.getUIContext().getHostContext()?.resourceManager.getRawFd('video_sample.mp4').then((inputDesc) => { + let outputFile = fileIo.openSync(this.getUIContext().getHostContext()?.cacheDir + '/transcoding.mp4', + fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + this.isTranscoding = true; + transcoding.startNative(inputDesc.fd, outputFile.fd, inputDesc.offset, inputDesc.length, + this.videoDataModel.videoCodecMime, this.videoDataModel.frameRate, this.videoDataModel.bitRate, + () => { + this.isTranscoding = false; + this.dialogController.close(); + this.pageStack.pushPathByName('VideoPlayer', ''); + }) + }); } } \ No newline at end of file diff --git a/entry/src/main/ets/pages/VideoPlayer.ets b/entry/src/main/ets/pages/VideoPlayer.ets index a91c69c..33314fa 100644 --- a/entry/src/main/ets/pages/VideoPlayer.ets +++ b/entry/src/main/ets/pages/VideoPlayer.ets @@ -32,10 +32,14 @@ export struct VideoPlayer { private controller: VideoController = new VideoController(); private transController: VideoController = new VideoController(); + // onBackPress() { + // this.pageInfos.popToIndex(-1); + // } + build() { Column() { Row() { - Button('转码前') + Button($r('app.string.before_transcoding')) .onClick(() => { this.tabsController.changeIndex(1); this.currentIndex = 1; @@ -45,7 +49,7 @@ export struct VideoPlayer { .backgroundColor(this.backgroundColorTwo) .margin(2) .width(80) - Button('转码后') + Button($r('app.string.after_transcoding')) .onClick(() => { this.tabsController.changeIndex(0); this.currentIndex = 0; @@ -65,7 +69,10 @@ export struct VideoPlayer { TabContent() { Column() { Stack() { - Video({ src: 'file://' + AppStorage.get('transcodingFile'), controller: this.transController }) + Video({ + src: 'file://' + this.getUIContext().getHostContext()?.cacheDir + '/transcoding.mp4', + controller: this.transController + }) .width('100%') .height('100%') .autoPlay(true) @@ -136,7 +143,7 @@ export struct VideoPlayer { TabContent() { Column() { Stack() { - Video({ src: AppStorage.get('selectedFile'), controller: this.controller }) + Video({ src: $rawfile('video_sample.mp4'), controller: this.controller }) .width('100%') .height('100%') .autoPlay(true) diff --git a/entry/src/main/ets/pages/VideoTranscoding.ets b/entry/src/main/ets/pages/VideoTranscoding.ets deleted file mode 100644 index 03c47c8..0000000 --- a/entry/src/main/ets/pages/VideoTranscoding.ets +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright (c) 2025 Huawei Device Co., Ltd. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { formatVideoTime } from '../common/utils/TimeUtils'; -import { VideoDataModel } from '../model/VideoDataModel'; -import { CommonConstants as Const } from '../common/CommonConstants'; -import transcoding from 'libtranscoding.so'; -import Logger from '../common/utils/Logger'; -import { fileIo } from '@kit.CoreFileKit'; -import { LoadingDialog } from '@kit.ArkUI'; - -const TAG: string = Const.INDEX_TAG; - -@Component -export struct VideoTranscoding { - @Consume('NavPathStack') pageStack: NavPathStack; - @State currentTime: number = 0; - @State durationTime: number = 0; - @State isStart: boolean = true; - @State selectedFilesDir: string = ''; - @State isTranscoding: boolean = false; - isConfig: boolean = false; - private videoDataModel: VideoDataModel = new VideoDataModel(); - private controller: VideoController = new VideoController(); - customDialogId: number = 0; - dialogController: CustomDialogController = new CustomDialogController({ - builder: LoadingDialog({ - content: '视频转码中' - }) - }); - - @Builder - customDialogComponent() { - Column() { - Text('视频转码中') - .fontSize(20) - .height(80) - Progress({ value: 0, total: 100, type: ProgressType.Ring }) - .margin({ top: 16, bottom: 16 }) - .width(120) - .color(Color.Blue) - .style({ strokeWidth: 20, status: ProgressStatus.LOADING }) - Blank().width(50) - Button("关闭") - .width('100%') - .margin(8) - .onClick(() => { - this.getUIContext().getPromptAction().closeCustomDialog(this.customDialogId); - transcoding.stopNative(); - }) - }.padding(20) - } - - build() { - Column() { - Column() { - Stack() { - Video({ src: this.selectedFilesDir, controller: this.controller }) - .width('100%') - .height('100%') - .autoPlay(true) - .controls(false) - .objectFit(1) - .zIndex(0) - .onAppear(() => { - this.selectedFilesDir = AppStorage.get('selectedFile') as string; - console.log('selectedFilesDir:' + this.selectedFilesDir); - }) - .onPrepared((event) => { - if (event) { - this.durationTime = event.duration - console.info(this.durationTime.toString()); - } - }) - .onUpdate((event) => { - if (event) { - this.currentTime = event.time - } - }) - .onFinish(() => { - this.isStart = !this.isStart; - }) - .transition(TransitionEffect.OPACITY.animation({ duration: 1000, curve: Curve.Sharp })) - - Row() { - Image(this.isStart ? $r('app.media.pause') : $r('app.media.play')) - .width(18) - .height(18) - .onClick(() => { - if (this.isStart) { - this.controller.pause(); - this.isStart = !this.isStart; - } else { - this.controller.start(); - this.isStart = !this.isStart; - } - }) - - Text(formatVideoTime(this.currentTime)) - .fontColor(Color.White) - .fontSize(12) - .margin({ left: '12vp' }) - Slider({ - value: this.currentTime, - min: 0, - max: this.durationTime - }) - .onChange((value: number, mode: SliderChangeMode) => { - // Set the video playback progress to jump to the value - this.controller.setCurrentTime(value); - }) - .layoutWeight(1) - Text(formatVideoTime(this.durationTime)) - .fontColor(Color.White) - .fontSize(12) - } - .padding({ left: '16vp', right: '16vp' }) - .zIndex(1) - .height(43) - .width('100%') - } - .alignContent(Alignment.Bottom) - .width('100%') - .layoutWeight(1) - } - .backgroundColor('') - .width('100%') - .layoutWeight(1) - - Blank() - - Column() { - Button($r('app.string.config_video')) - .id('config') - .onClick(() => { - this.getUIContext().showTextPickerDialog({ - defaultPickerItemHeight: Const.DEFAULT_PICKER_ITEM_HEIGHT, - selectedTextStyle: ({ - font: ({ - size: Const.SELECTED_TEXT_STYLE_FONT_SIZE - }) - }), - range: Const.RECORDER_INFO, - canLoop: false, - alignment: DialogAlignment.Center, - onAccept: (value: TextPickerResult) => { - switch (value.value[0]) { - case Const.VIDEO_MIMETYPE[0]: { - this.videoDataModel.setCodecFormat(Const.TRUE, Const.MIME_VIDEO_AVC); - break; - } - case Const.VIDEO_MIMETYPE[1]: { - this.videoDataModel.setCodecFormat(Const.FALSE, Const.MIME_VIDEO_HEVC); - break; - } - default: - break; - } - - switch (value.value[1]) { - case Const.VIDEO_RESOLUTION[0]: { - this.videoDataModel.setResolution(Const.VIDEO_WIDTH_4K, Const.VIDEO_HEIGHT_4K, - Const.BITRATE_VIDEO_30M); - break; - } - case Const.VIDEO_RESOLUTION[1]: { - this.videoDataModel.setResolution(Const.VIDEO_WIDTH_1080P, Const.VIDEO_HEIGHT_1080P, - Const.BITRATE_VIDEO_20M); - break; - } - case Const.VIDEO_RESOLUTION[2]: { - this.videoDataModel.setResolution(Const.VIDEO_WIDTH_720P, Const.VIDEO_HEIGHT_720P, - Const.BITRATE_VIDEO_10M); - break; - } - default: - break; - } - - switch (value.value[2]) { - case Const.VIDEO_FRAMERATE[0]: { - this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_30FPS; - break; - } - case Const.VIDEO_FRAMERATE[1]: { - this.videoDataModel.frameRate = Const.FRAMERATE_VIDEO_60FPS; - break; - } - default: - break; - } - } - }); - - }) - .size({ - width: '100%', - height: 40 - }) - .margin({ - bottom: 16 - }) - - Button('开始转码') - .onClick(() => { - this.dialogController.open(); - this.transcoding(); - }) - .width('100%') - } - .padding(16) - } - .width('100%') - .height('100%') - .justifyContent(FlexAlign.Start) - } - - transcoding() { - let inputFile = fileIo.openSync(this.selectedFilesDir, fileIo.OpenMode.READ_ONLY); - AppStorage.setOrCreate('transcodingFile', this.getUIContext().getHostContext()?.cacheDir + '/transcoding.mp4'); - let outputFile = fileIo.openSync(this.getUIContext().getHostContext()?.cacheDir + '/transcoding.mp4', - fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); - if (!inputFile) { - Logger.error(TAG, 'transcoding inputFile is null'); - } - let inputFileState = fileIo.statSync(inputFile.fd); - if (inputFileState.size <= 0) { - Logger.error(TAG, 'transcoding inputFile size is 0'); - } - this.isTranscoding = true; - transcoding.startNative(inputFile.fd, outputFile.fd, Const.DEFAULT_VALUE, inputFileState.size, - this.videoDataModel.videoCodecMime, this.videoDataModel.width, this.videoDataModel.height, - this.videoDataModel.frameRate, this.videoDataModel.bitRate, - () => { - this.isTranscoding = false; - this.dialogController.close(); - fileIo.close(inputFile); - this.pageStack.pushPathByName('VideoPlayer', ''); - }) - } -} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index 156a926..05b9339 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -44,6 +44,18 @@ "name": "start_transcoding", "value": "Video Transcoding" }, + { + "name": "transcoding", + "value": "Transcoding..." + }, + { + "name": "before_transcoding", + "value": "Before" + }, + { + "name": "after_transcoding", + "value": "After" + }, { "name": "alert_setting", "value": "The setting is not supported by the current device." diff --git a/entry/src/main/resources/en_US/element/string.json b/entry/src/main/resources/en_US/element/string.json index 7eef904..d0ff7a1 100644 --- a/entry/src/main/resources/en_US/element/string.json +++ b/entry/src/main/resources/en_US/element/string.json @@ -24,6 +24,18 @@ "name": "video_transcoding", "value": "Video Transcoding" }, + { + "name": "transcoding", + "value": "Transcoding..." + }, + { + "name": "before_transcoding", + "value": "Before" + }, + { + "name": "after_transcoding", + "value": "After" + }, { "name": "video_player", "value": "Video Player" diff --git a/entry/src/main/resources/rawfile/video_sample.mp4 b/entry/src/main/resources/rawfile/video_sample.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..b529170de5433cb2ec4a9d092d923ffd9e3583a4 GIT binary patch literal 10467141 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