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/2] =?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/2] =?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