diff --git a/code/BasicFeature/Media/AVCodec/.gitignore b/code/BasicFeature/Media/AVCodec/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..fbabf771011fe78f9919db0b1195ab6cadffc2b0 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/.gitignore @@ -0,0 +1,11 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/AppScope/app.json5 b/code/BasicFeature/Media/AVCodec/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..1bafa013f53730b51b5d8d84a23666f8f916efff --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/AppScope/app.json5 @@ -0,0 +1,25 @@ +/* + * 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": { + "bundleName": "com.samples.avcodecsample", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/code/BasicFeature/Media/AVCodec/AppScope/resources/base/element/string.json b/code/BasicFeature/Media/AVCodec/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..8427310926f445f5313595f2990e44c6ced2af54 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "AVCodec" + } + ] +} diff --git a/code/BasicFeature/Media/AVCodec/AppScope/resources/base/media/app_icon.png b/code/BasicFeature/Media/AVCodec/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cd45accb1dfd2fd0da16c732c72faa6e46b26521 Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/AppScope/resources/base/media/app_icon.png differ diff --git a/code/BasicFeature/Media/AVCodec/README_zh.md b/code/BasicFeature/Media/AVCodec/README_zh.md new file mode 100644 index 0000000000000000000000000000000000000000..72aaf01dcd79fed3c198ef132cd74cb4e4710866 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/README_zh.md @@ -0,0 +1,568 @@ +# AVCodecSample + +### 介绍 + +AVCodec 部件示例 Sample,基于 API12 构建,提供视频播放(含音频)和录制的功能。 + +- 视频播放的主要流程是将视频文件通过解封装->解码->送显/播放。 +- 视频录制的主要流程是相机采集->编码->封装成mp4文件。 + +### 播放支持的原子能力规格 + +| 媒体格式 | 封装格式 | 码流格式 | +|------|:-----------------|:-------------------------------------------------------------------------------------------------------------------------------------------------| +| 视频 | mp4、mkv、mpeg-ts等 | 视频码流:
- 硬解:AVC(H.264)、HEVC(H.265)
- 软解:MPEG2、MPEG4、H.263、AVC(H.264)
音频码流:
AAC、MPEG(MP3)、Flac、Vorbis、AMR(amrnb、amrwb)、G711mu、APE | + +更多格式[参考](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/media/avcodec/avcodec-support-formats.md#avcodec%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F) + +### 录制支持的原子能力规格 + +| 封装格式 | 视频编解码类型 | +|------|-------------------------| +| mp4 | HEVC(H.265)、 AVC(H.264) | + +注意,目前仅支持视频录制,未集成音频能力,更多格式[参考](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/media/avcodec/avcodec-support-formats.md#avcodec%E6%94%AF%E6%8C%81%E7%9A%84%E6%A0%BC%E5%BC%8F) + +### 效果预览 + +| 播放(模式选择) | 播放(选择播放路径) | 播放(横屏) | 播放(竖屏) | +|-------------------------------------------|-----------------------------------------------|---------------------------------------|---------------------------------------| +| ![播放_模式选择.jepg](screenshots/播放_模式选择.jpeg) | ![播放_选择播放路径.jpeg](screenshots/播放_选择播放路径.jpeg) | ![播放_横屏.jpeg](screenshots/播放_横屏.jpeg) | ![播放_竖屏.jpeg](screenshots/播放_竖屏.jpeg) | + +| 播放(倍速) | 录制(选择相机分辨率) | 录制(开始录制) | +|---------------------------------------|-------------------------------------------------|-------------------------------------------| +| ![播放_倍速.jpeg](screenshots/播放_倍速.jpeg) | ![录制_选择相机分辨率.jpeg](screenshots/录制_选择相机分辨率.jpeg) | ![录制_开始录制.jpeg](screenshots/录制_开始录制.jpeg) | + +### 使用说明 + +弹出是否允许“AVCodec”使用相机?点击“允许” + +- 推送视频到文件管理? + hdc file send xx.xx storage/media/100/local/files/Docs +- 推送视频到图库? + hdc file send xx.mp4 storage/media/100/local/files + hdc shell mediatool send /storage/media/100/local/files/xx.mp4 + +#### 播放 + +1. 推送文件到本地(可单独音频、单独视频、视频含音频)或点击下方“录制”,录制一个视频文件(无音频) + +2. 点击播放按钮,选择从文件管理选取或从图库选取,点击确定,选择文件播放 + +3. 播放过程中,可长按播放窗口2倍速播放,松开原速播放,或点击播放按钮,选择指定倍速播放 + +#### 录制 + +1. (可选)设置-配置相机参数 + +2. 点击“录制” + +3. 点击“保存” + +4. 点击“开始录制” + +5. 点击“停止录制” + +### 目录 + +仓目录结构如下: + +``` +video-codec-sample/entry/src/main/ +├── cpp # Native层 +│ ├── capbilities # 能力接口和实现 +│ │ ├── include # 能力接口 +│ │ ├── audio_decoder.cpp # 音频解码实现 +│ │ ├── demuxer.cpp # 解封装实现 +│ │ ├── muxer.cpp # 封装实现 +│ │ ├── video_decoder.cpp # 视频解码实现 +│ │ └── video_encoder.cpp # 视频编码实现 +│ ├── common # 公共模块 +│ │ ├── dfx # 日志 +│ │ ├── sample_callback.cpp # 编解码回调实现 +│ │ ├── sample_callback.h # 编解码回调定义 +│ │ └── sample_info.h # 功能实现公共类 +│ ├── render # 送显模块接口和实现 +│ │ ├── include # 送显模块接口 +│ │ ├── plugin_manager.cpp # 送显模块管理实现 +│ │ └── plugin_render.cpp # 送显逻辑实现 +│ ├── sample # Native层 +│ │ ├── player # Native层播放接口和实现 +│ │ │ ├── Player.cpp # Native层播放功能调用逻辑的实现 +│ │ │ ├── Player.h # Native层播放功能调用逻辑的接口 +│ │ │ ├── PlayerNative.cpp # Native层 播放的入口 +│ │ │ └── PlayerNative.h # +│ │ └── recorder # Native层录制接口和实现 +│ │ ├── Recorder.cpp # Native层录制功能调用逻辑的实现 +│ │ ├── Recorder.h # Native层录制功能调用逻辑的接口 +│ │ ├── RecorderNative.cpp # Native层 录制的入口 +│ │ └── RecorderNative.h # +│ ├── types # Native层暴露上来的接口 +│ │ ├── libplayer # 播放模块暴露给UI层的接口 +│ │ └── librecorder # 录制模块暴露给UI层的接口 +│   └── CMakeLists.txt # 编译入口 +├── ets # UI层 +│ ├── common # 公共模块 +│ │ └──utils # 共用的工具类 +│ │ ├── CameraCheck.ets # 相机能力查询 +│ │ ├── DateTimeUtils.ets # 获取当前时间 +│ │ └── Logger.ts # 日志工具 +│ ├── CommonConstants.ets # 参数常量 +│ ├── entryability # 应用的入口 +│ │   └── EntryAbility.ts # 申请权限弹窗实现 +│ ├── pages # EntryAbility 包含的页面 +│ │ └── Index.ets # 首页/播放页面 +│   └── sample # sample +│ └── recorder # 录制 +│ └── Recorder.ets # 录制页面 +├── resources # 用于存放应用所用到的资源文件 +│ ├── base # 该目录下的资源文件会被赋予唯一的ID +│ │ ├── element # 用于存放字体和颜色 +│ │ ├── media # 用于存放图片 +│ │ └── profile # 应用入口首页 +│ ├── en_US # 设备语言是美式英文时,优先匹配此目录下资源 +│ └── zh_CN # 设备语言是简体中文时,优先匹配此目录下资源 +└── module.json5 # 模块配置信息 +``` + +### 具体实现 + +#### *视频播放* + +- 应用启动,Xcomponent加载, 触发OnSurfaceCreatedCB(), 此时能拿到一个surface,同时调用OH_NativeWindow_NativeWindowSetScalingModeV2接口给window配置一个自适应等比例拉伸原图像尺寸的Key,后续无论播放横屏视频还是竖屏视频,都不用更改XComponent的尺寸。 +- 点击播放,选择文件后,能拿到文件fd,fileSize,根据拿到的fd和fileSize创建解封装器。 +- 根据解封装器从文件中拿到的文件属性,创建对应的解码器,若走解码器的Surface模式,则要把之前拿到的surface也配置给解码器。 +- 调用解码器Start,开始buffer轮转。buffer轮转时,由于buffer数量有上限,需要各个模块及时消费收到的buffer,否则会影响整体速度。 +- 解码器Start调用后,首先会触发4次输入回调,里面会给应用OH_AVBuffer和其对应的index。 +- 应用需要把待解码的码流,一帧帧填充到输入回调给到应用的OH_AVBuffer里的buffer地址里,然后调用OH_VideoDecoder_PushInputBuffer,传给解码器。 + ```text + 本示例里使用的是解封装器一键填充配置,若码流直送解码器: + (1) 需要调用OH_AVBuffer_GetAddr获取OH_AVBuffer内buffer的内存地址,以进行之后的拷贝。 + (2) 可以调用OH_AVBuffer_GetCapacity获取OH_AVBuffer内buffer容量大小,避免拷贝越界。 + (3) 必须调用OH_AVBuffer_SetBufferAttr配置实际拷贝到OH_AVBuffer内buffer的实际size,按照规范,pts,offset,flags最好也配置对。 + (4) 给解码器的输入,要保证以下三点,才能正常解码: + ①当前仅支持传入annexB格式帧,不支持avcc格式帧。 + ②确保buffer size正确传入。 + ③首帧要传XPS信息。(可以pps、sps和I帧同时传,也可以先传pps、sps,再传I帧) + 仅关键帧(I帧):AVCODEC_BUFFER_FLAGS_SYNC_FRAME + 仅配置帧(pps,sps):AVCODEC_BUFFER_FLAGS_CODEC_DATA + 是配置帧又是关键帧:AVCODEC_BUFFER_FLAGS_CODEC_DATA|AVCODEC_BUFFER_FLAGS_SYNC_FRAME + 普通帧(P帧):AVCODEC_BUFFER_FLAGS_NONE + ``` +- 待传给解码器解码完成后,会触发输出回调给应用。 + - 若是Surface模式,则输出侧实际的buffer只会在框架、解码器、surface侧轮转,回调给应用的OH_AVBuffer只是个壳子,里面会带一些flag,size等信息,以及应用在输入时配置的pts信息,但由于实际的buffer不会随着OH_AVBuffer回调给用户,所以Surface模式下调用OH_AVBuffer_GetAddr拿不到buffer的地址,不能直接拷贝解码后的buffer数据,如果有这个需求,则需把surface配置成NativeImage的window,调用NativeImage的接口获取。 + - 若是Buffer模式,实际的buffer会通过回调给到应用。由于buffer模式没法通过解码直接送显,本示例会将解码后的yuv/rgba图像dump到应用的沙箱目录/data/app/el2/100/base/com.samples.avcodecsample/haps/entry/files/haps/entry/files/下,应用可将此文件提取上来检验效果。 + ```text + Surface里维护着一个surfaceBuffer队列,供生产者、消费者轮换使用,且生产者和消费者往往不在同一个进程。 + - 生产者的逻辑: + · RequestBuffer:生产者获取一个空闲的、可以往里填数据的buffer,同时获取出这个buffer对应的releaseFence。当fence等到后,生产者可以往这块buffer里生产数据 + · FlushBuffer:生产者生产完后,将该buffer以及该buffer对应的acquireFence送回给surface。 + · CancelBuffer:生产者没有向该buffer里生产数据,仅做归还。 + - 消费者的逻辑: + · AcquireBuffer: 消费者获取一个已生产好的buffer,同时获取出这个buffer对应的acquireFence。当fence等到后,消费者可以开始读取这块buffer里的内容。 + · ReleaseBuffer:消费者消费完成后将buffer以及该buffer对应的releaseFence归还给surface。 + ``` +- 应用收到解码后的OH_AVBuffer后,需要及时调用OH_VideoDecoder_FreeOutputBuffer或OH_VideoDecoder_RenderOutputBuffer或OH_VideoDecoder_RenderOutputBufferAtTime释放归还buffer。 + ```text + 用RK3568设备播放,由于调用OH_NativeWindow_NativeWindowSetScalingModeV2接口后,实际未生效,最后显示画面可能会有拉伸,应用可以用另一种方法解决: + 在解封装拿到视频的宽高信息后,回调到UI层,UI层根据这个宽高,更改XComponet的尺寸,达到一样的效果,参考如下代码: + import display from '@ohos.display' + private display = display.getDefaultDisplaySync() + @State xcomponentHeight: number | string | Resource = 1 + @State xcomponentWidth: number | string | Resource = 1 + + if (data.videoWidth / data.videoHeight > this.display.width / this.display.height) { + this.xcomponentHeight = (this.display.width * data.videoHeight / data.videoWidth) + 'px'; + this.xcomponentWidth = this.display.width + 'px'; + } else { + this.xcomponent = this.display.height + 'px'; + this.xcompoentWidth = (this.display.height * data.videoWidth / data.videoHeight) + 'px' + } + ``` + + +##### Buffer轮转 +一、surface的buffer轮转 + +轮转方最多有四个: +- us : hcodec/框架自己 +- user : hcodec的调用者 +- omx : vendor编解码器 + +surface: 在解码时,surface指消费者;编码时,surface指生产者 + +以解码器surface模式的某个输出buffer为例: + +一开始是hcodec分配出来————owned by us + +然后给到vendor(即us->omx)————owned by omx + +vendor填好后还给hcodec(即omx->us)————owned by us + +hcodec给到应用(即us->user)————owned by user + +app等到音画同步后还给hcodec(即user->us)————owned by us + +hcodec flushBuffer给surface, 让消费者消费(即us->surface)————owned by surface + +消费者消费完后,hcodec去requestBuffer(即surface->us)————owned by us + +给vendor(即us->omx)————owned by omx + +其他模式不再赘述,直接看图表 + +![img_2.png](screenshots/img_2.png) + +#### *录制* + +- 点击“设置”(可选),设置相应的规格后,本示例会先校验当前的相机是否支持输出该规格的流,不支持则更改为默认的1080P的流,若1080P的流仍不支持,则更改为相机能输出配置流的第一个配置。 +- 点击录制后,确定保存后,本示例会根据用户设置选择的配置(未选择则默认1080P),首先创建一个该配置对应的编码器,同时创建好封装器,Surface模式下,编码器OH_VideoEncoder_GetSurface接口,会给应用一个OHNativeWindow **window,来接收编码输入。 +- 使用这个window,调用OH_NativeWindow_GetSurfaceId接口,能拿到window对应的surface的surfaceId,此surfaceId用做相机的录像流的输出surfaceId。 +- 把surfaceId回调到UI层,UI层拿到surfaceId后,携带主页配置的参数信息和surfaceId,路由跳转到录制页面。 +- 录制页面构建时,XComponent构建时,会触发.onLoad()方法,此时能拿到Xcomonent对应的surface的surfaceId,此surfaceId用做相机的预览流的输出surfaceId。 +- 有了两个surfaceId,和想要的相机配置,就能开始创建相机,开始录像了。 +- 参考上文的buffer轮转,本示例在XComponent.onLoad()触发后,建立了一个相机生产,XComponent消费相机预览流,编码器消费相机录像流的生产消费模型,但此时编码器还未开始消费。 +- 待用户在录像页面点击“开始录制”后,本示例才会调用编码器的OH_VideoEncoder_Start()方法,开始录像编码。 +- 编码Surface,由surface生产端(本例是相机)直接往surface内flush SurfaceBuffer,surface内收到buffer后,编码器自动开始编码。 +- 待编码完成后,会触发输出回调给应用,里面会带有每帧编码后的OH_AVBuffer和其对应的index。 +- 此时应用可以调用OH_AVBuffer_GetAddr获取OH_AVBuffer内buffer的内存地址,OH_AVBuffer_GetBufferAttr获取编码后buffer的size等参数信息,本例是直接通过封装器,将其写入文件帧。 + ```text + 若使用RK3568相机录制,相机输出RGBA格式流到编码器Surface,实际flush到Surface里的buffer画面异常,导致最后的录像文件,播放起来的效果不对。 + ``` + +![img_8.png](screenshots/img_8.png) + +### 音画同步 + +#### 前言 + +##### 背景和目的 + +目前手机播放器在输出设备为蓝牙耳机时会出现严重音视频不同步现象, 严重影响用户体验。本文旨在指导第三方视频播放应用正确获取并使用音频相关信息来保证音视频同步。 + +精确的音视频同步是媒体播放的关键性能指标之一。一般来说,在录音设备上同时录制的音频和视频需要在播放设备(例如手机,电视,媒体播放器)上同时播放。为了实现设备上的音视频同步,可以按如下指南操作。 + +##### 概念定义 + +| Abbreviations缩略语 | Full spelling 英文全名 | Chinese explanation 中文解释 | +|------------------|:------------------------|:-------------------------| +| PTS | Presentation Time Stamp | 送显时间戳 | +| DTS | Decoding Time Stamp | 解码时间戳 | + +- DTS(解码时间戳) + 指音视频数据在解码器中开始解码的时间戳。它表示解码器应该从输入数据流中读取和解码的特定时间点。DTS用于控制解码器的解码顺序,确保音视频数据按照正确的顺序解码。 +- PTS(显示时间戳) + 指音视频数据在播放时应该显示给用户的时间戳。它表示解码后的音视频数据在播放时应该出现在屏幕上或传递给音视频输出设备的时间点。PTS用于控制音视频的播放顺序和时序,以确保音视频在正确的时间点进行显示或播放。 + +##### 音画同步原理 + +音视频数据的最小处理单元称为帧。音频流和视频流都被分割成帧,所有帧都被标记为需要按特定的时间戳显示。音频和视频可以独立下载和解码,但就具有匹配时间戳的音频和视频帧应同时呈现,达到A/V同步的效果。 + +![img.png](screenshots/img.png) + +理论上,因为音频通路存在时延,匹配音频和视频处理,有三种A/V同步解决方案可用; + +(1)连续播放音频帧:使用音频播放位置作为主时间参考,并将视频播放位置与其匹配。 + +(2)使用系统时间作为参考:将音频和视频播放与系统时间匹配。 + +(3)使用视频播放作为参考:让音频匹配视频。 + + +| 策略名称 | 优点 | 缺点 | +|-------------|:----------------------------------------------------|:-------------------------------------------------------------------------| +| 连续播放音频帧(推荐) | ①用户肉眼的敏感度较弱,不易察觉视频微小的调整。
②容易实现,因为视频刷新时间的调整相对容易。 | ①如果视频帧率不稳定或延迟渲染大,可能导致视频卡顿或跳帧。 | +| 使用系统时间作为参考 | 可以最大限度地保证音频和视频都不发生跳帧行为。 | ①需要额外依赖系统时钟,增加系统复杂性和维护成本。
②系统时钟的准确性对同步效果影响较大,如果系统时钟不准确,可能导致同步效果大打折扣。 | +| 使用视频播放作为参考 | 音频可以根据视频帧进行调整,减少音频跳帧的情况。 | ①音频播放可能会出现等待或加速的情况,相较于视频,会对用户的影响更为严重和明显。
②如果视频帧率不稳定,可能导致音频同步困难。 | + +第一个选项是唯一一个具有连续音频数据流的选项,没有对音频帧的显示时间、播放速度或持续时间进行任何调整。这些参数的任何调整都很容易被人的耳朵注意到,并导致干扰的音频故障,除非音频被重新采样;但是,重新采样也会改变音调。因此,一般的多媒体应用使用音频播放位置作为主时间参考。以下段落将讨论此解决方案。(其它两个选项不在本文档的范围内) + +### 效果展示 + +#### 场景说明 + +#### 适用范围 + +适用于应用中视频播放过程中,由于设备渲染延迟、播放链路异常导致的音画不同步的场景 + +##### 场景体验指标 + +音画同步标准 + +① 为了衡量音画同步的性能,用对应音频和视频帧实际播放时间的差值作为数值指标,数值大于0表示声音提前画面,小于0表示声音落后画面。 + +② 最大卡顿时长,单帧图像停滞时间超过100ms的,定义为卡顿一次。连续测试5分钟,建议设置为100ms。 + +③ 平均播放帧率,平均每秒播放帧数,不反映每帧显示时长。 + +测试基准:一倍速场景 + +| | 范围 | 主观体验 | +|--------|:---------------|:-----| +| S标(建议) | [-80ms, 25ms] | 无法察觉 | +| A标 | [-125ms, 45ms] | 能够察觉 | +| B标 | [-185ms, 90ms] | 能够察觉 | + +| 描述 | 应用内播放视频,音画同步指标应满足[-125ms, 45ms]。 | +|------|:---------------------------------| +| 类型 | 规则 | +| 适用设备 | 手机、折叠屏、平板 | +| 说明 | 无 | + +#### 场景分析 + +##### 典型场景及优化方案 + +**典型场景描述** +应用内播放视频,音画同步指标应满足[-80ms, 25ms]. +**场景优化方案** +该解决方案使用: + +- 视频同步到音频(主流方案) +- 获取音频渲染进度动态调整视频渲染进度 + +最终实现音画同步[-80ms,25ms]的效果。 + + +**图2 音画同步示意图** + +![img_1.png](screenshots/img_1.png) + +#### 场景实现 + +##### 场景整体介绍 + +音频和视频的管道必须同时以相同的时间戳呈现每帧数据。音频播放位置用作主时间参考,而视频管道只输出与最新渲染音频匹配的视频帧。对于所有可能的实现,精确计算最后一次呈现的音频时间戳是至关重要的。OS提供API来查询音频管道各个阶段的音频时间戳和延迟。 + +音频管道支持查询最新呈现的时间戳,getTimeStamp() +方法提供了一种简单的方法来确定我们要查找的值。如果时间戳可用,则audioTimestamp实例将填充以帧单位表示的位置,以及显示该帧时的估计时间。此信息可用于控制视频管道,使视频帧与音频帧匹配。 + +##### 接口说明 + +```cpp +/* + * Query the the time at which a particular frame was presented. + * + * @since 10 + * + * @param renderer Reference created by OH_AudioStreamBuilder_GenerateRenderer() + * @param clockId {@link #CLOCK_MONOTONIC} + * @param framePosition Pointer to a variable to receive the position + * @param timestamp Pointer to a variable to receive the timestamp + * @return Function result code: + * {@link AUDIOSTREAM_SUCCESS} If the execution is successful. + * {@link AUDIOSTREAM_ERROR_INVALID_PARAM}: + * 1.The param of renderer is nullptr; + * 2.The param of clockId invalid. + * {@link AUDIOSTREAM_ERROR_ILLEGAL_STATE} Execution status exception. + */ +OH_AudioStream_Result OH_AudioRenderer_GetTimestamp(OH_AudioRenderer* renderer, + clockid_t clockId, int64_t* framePosition, int64_t* timestamp); +``` + +注意事项: + +(1) +OH_AudioRenderer_Start到真正写入硬件有一定延迟,因此该接口在OH_AudioRenderer_Start之后过一会儿才会再拿到有效值,期间音频未发声时建议画面帧先按照正常速度播放,后续再逐步追赶音频位置从而提升用户看到画面的起搏时延。 + +(2)当framePosition和timeStamp稳定之前,调用可以比较频繁(如100ms) +,当以稳定的速度增长前进后,建议OH_AudioRenderer_GetTimestamp的频率不要太频繁,可以每分钟一次,最好不要低于500ms一次,因为频繁调用可能会带来功耗问题,因此在能保证音画同步效果的情况下,不需要频繁地查询时间戳。 + +(3)OH_AudioRenderer_Flush接口执行后,framePosition返回值会重新(从0)开始计算。 + +(4)OH_AudioRenderer_GetFramesWritten 接口在Flush的时候不会清空,该接口和OH_AudioRenderer_GetTimestamp接口并不建议配合使用。 + +(5)音频设备切换过程中OH_AudioRenderer_GetTimestamp返回的framePosition和timestamp不会倒退,但由于新设备写入有时延,会出现短暂时间内音频进度无增长,建议画面帧保持流程播放不要产生卡顿。 + +(6) +OH_AudioRenderer_GetTimeStamp获取的是实际写到硬件的采样帧数,不受倍速影响。对AudioRender设置了倍速的场景下,播放进度计算需要特殊处理,系统保证应用设置完倍速播放接口后,新写入AudioRender的采样点才会做倍速处理。 + +##### 关键代码片段 + +(1)获取音频渲染的位置 + +```cpp +// get audio render position +int64_t framePosition = 0; +int64_t timestamp = 0; +int32_t ret = OH_AudioRenderer_GetTimestamp(audioRenderer_, CLOCK_MONOTONIC, &framePosition, ×tamp); +AVCODEC_SAMPLE_LOGI("VD framePosition: %{public}li, nowTimeStamp: %{public}li", framePosition, nowTimeStamp); +audioTimeStamp = timestamp; // ns +``` + +(2)音频启动前暂不做音画同步 + +- 音频未启动前,timestamp和framePosition返回结果为0,为避免出现卡顿等问题,暂不同步 + +```cpp +// audio render getTimeStamp error, render it + if (ret != AUDIOSTREAM_SUCCESS || (timestamp == 0) || (framePosition == 0)) { + DumpOutput(bufferInfo); + // first frame, render without wait + ret = videoDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, sampleInfo_.codecRunMode ? false : true, + GetCurrentTime()); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGW("FreeOutputBuffer failed: %{public}d", ret); + return false; + } + std::this_thread::sleep_until(lastPushTime + std::chrono::microseconds(sampleInfo_.frameInterval)); + lastPushTime = std::chrono::system_clock::now(); + return true; + } +``` + +(3)根据视频帧pts和音频渲染位置计算延迟waitTimeUs + +- audioPlayedTime音频帧期望渲染时间 +- videoPlayedTime视频帧期望送显时间 + +```cpp +// after seek, audio render flush, framePosition = 0, then writtenSampleCnt = 0 +int64_t latency = (audioDecContext_->frameWrittenForSpeed - framePosition) * 1000 * + 1000 / sampleInfo_.audioSampleRate / speed; +AVCODEC_SAMPLE_LOGI("VD latency: %{public}li writtenSampleCnt: %{public}li", latency, writtenSampleCnt); + +nowTimeStamp = GetCurrentTime(); +int64_t anchordiff = (nowTimeStamp - audioTimeStamp) / 1000; + +// us, audio buffer accelerate render time +int64_t audioPlayedTime = audioDecContext_->currentPosAudioBufferPts - latency + anchorDiff; +// us, video buffer expected render time +int64_t videoPlayedTime = bufferInfo.attr.pts; + +// audio render timestamp and now timestamp diff +int64_t waitTimeUs = videoPlayedTime - audioPlayedTime; // us +``` + +(4)根据业务延迟做音画同步策略 + +- [,-40ms) 视频帧较晚,此帧丢掉 +- [-40ms,0ms)视频帧直接送显 +- [0ms,)视频帧较早,根据业务需要选择现象追帧 + +```cpp +// video buffer is too late, drop it +if (waitTimeUs < WAIT_TIME_US_THRESHOLD_WARNING) { + dropFrame = true; + AVCODEC_SAMPLE_LOGI("VD buffer is too late"); +} else { + AVCODEC_SAMPLE_LOGE("VD buffer is too early waitTimeUs:%{public}ld", waitTimeUs); + // [0, ), render it wait waitTimeUs, max 1s + // [-40, 0), render it + if (waitTimeUs > WAIT_TIME_US_THRESHOLD) { + waitTimeUs = WAIT_TIME_US_THRESHOLD; + } + // per frame render time reduced by frame interval + if (waitTimeUs > sampleInfo_.frameInterval + perSinkTimeThreshold) { + waitTimeUs = sampleInfo_.frameInterval + perSinkTimeThreshold; + AVCODEC_SAMPLE_LOGE("VD buffer is too early and reduced, waitTimeUs: %{public}ld", waitTimeUs); + } +} +``` + +(5)进行音画同步 +若视频帧的时间大于2倍vsync的时间,则需要sleep超过的时间。 + +```cpp +if (static_cast(waitTimeUs) > VSYNC_TIME * LIP_SYNC_BALANCE_VALUE) { + std::this_thread::sleep_for(std::chrono::microseconds( + static_cast(static_cast(waitTimeUs) - VSYNC_TIME * LIP_SYNC_BALANCE_VALUE))); +} +DumpOutput(bufferInfo); +int32_t ret = videoDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, + sampleInfo_.codecRunMode ? false : !dropFrame, + VSYNC_TIME * LIP_SYNC_BALANCE_VALUE * MS_TO_S + GetCurrentTime()); +if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("FreeOutputBuffer failed: %{public}d", ret); + return false; +} +return true; +``` + +### 倍速播放方案 + +#### 当前问题 + +![img_4.png](screenshots/img_4.png) + +通过Audio GetTimeStamp拿到的Position始终是一倍速参考系下计算的,导致应用写下多倍速的音频帧后不清楚底层实际播放的原始位置。 + +**比如假设采样率是48k,应用写的frameIn A一共写了48000,2倍速后的frameOut A' 只有24000, +底层播了一半后返回给应用的position是12000 - 硬件latency(假设是100ms) +,也就是倍速后播了150ms,但应用实际播放的pts应该是24000-硬件latency×2 = 300ms** + +Position表示的是音频帧,一个音频帧包括左右声道的采样点交织形成的数据包,比如双声道16bit采样点,一帧数据是4个字节,48k采样率的音频,一秒播放48000帧 + +应用一般音画同步做法: + +视频每解码一帧,获取一下音频clock,视频帧永远跟随音频pts + +#### **倍速的音频时间戳计算算法(此方法也同样适用于三方自研播放器)** + +原理:记录每次setSpeed时的最后position状态作为基准,更新speed之后,按照上一次speed末尾的基准+数据delta×最新speed返回给应用 + +| **时间线** | **应用行为** | **播放范围(写给AudioRender的数据)** | **此刻音频服务处理的位置(frameOutC)** | **pulseaudio实际返回的position** | **audiorender矫正后返回给应用的值** | **音频PTS(假设起始时间是X)** | +|:---------------:|:--------:|:---------------------------------------:|:--------------------------:|:-----------------------------------------------------------------------------------------------------------------:|:-------------------------:|:----------------------------------------------------------------:| +| **T0时刻** | 先一倍速 | 1-1000 | 800 | 600 | 600 | X + 600/48000 | +| **T1时刻** | 倍速调节成2 | | | 记录倍速调节之前写的位置
lastSpeedX = 1000
lastSpeedFramesWritten = 1000 | | | +| **T2时刻** | 2倍速 | 原始数据1001-2000,倍速后送给Audio服务的是(1001-1500) | 1400 | 1200 | | | +| **计算T2时刻音频PTS** | | | | 1200如何倒推音源Position?
实际位置=(position-lastSpeedIdx)*speed + lastSpeedFramesWritten
(1200-1000)×2+1000 = 1400 | 1400 | X+1400/48000
记录lastPosition = 1400
lastPositionTime = T2 | +| **视频出帧T2'时刻** | | | | | | 送显delay = 视频PTS - (X + 1400 / 48000 + (T2' - T2)*2 | +| **T3时刻** | 倍速调节成3 | | | 记录倍速调节之前写的位置
lastSpeedIdx = 1500
lastSpeedFramesWritten = 2000 | | | +| **T4时刻** | 3倍速 | 原始数据2001-3500,倍速后送欸Audio服务的是(1501-2000) | 1600 | 1400 | | | +| **计算T4时刻音频PTS** | | | | 1400 < 1500, 说明底层还在播老倍速的数据,复用上一次的音频pts做偏移 | 1400+(T4-T2)×2 | X+(lastPosition+(T4-T2)×2) | +| **T5时刻** | 3倍速 | 原始数据2001-3500 播放中 | 1900 | 1700 | | | +| **计算T5时刻音频PTS** | | Content | | 实际位置=(position-lastSpeedIdx)*speed + lastSpeedFramesWritten
(1700-1500)×3+2000 = 2600 | 2600 | X+2600/48000 | + +### 环境配置 +#### OpenHarmony +切换OpenHarmony工程,签名后运行,右下角报错: + +![img_5.png](screenshots/img_5.png) + +通过文件-设置打开OpenHarmonySDK目录: + +![img_6.png](screenshots/img_6.png) + +根据你的SDK version找到Local\OpenHarmony\Sdk\13\ets\api\device-define文件夹(此例为13)的default.json + +![img_7.png](screenshots/img_7.png) + +这个就是你“default”类型的设备的system capability的要求 + +此例,缺这两个: + +SystemCapability.HiviewDFX.HiDumper, + +SystemCapability.Multimedia.AVSession.ExtendedDisplayCast. + +那就打开json文件,删掉这个要求,保存再编,就行了 + +#### HarmonyOS +若切换成HarmonyOS工程,搜索runtimeOS,将OpenHarmony字段改成HarmonyOS,上面的sdkVersion,改成"5.0.0(12)"这样的形式(保留双引号),搜索删除abiFilters字段后的"armeabi-v7a"参数。 + +### 相关权限 + +#### [ohos.permission.CAMERA](https://docs.openharmony.cn/pages/v4.1/zh-cn/application-dev/security/AccessToken/permissions-for-all.md#ohospermissioncamera) + +### 依赖 + +XComponent Camera + +### 约束与限制 + +1.本示例仅支持标准系统上运行,支持Phone, RK3568; + +2.本示例为Stage模型,仅支持 API12 及以上版本SDK, SDK版本号5.0.0.19及以上版本,镜像版本号支持5.0.0.19及以上版本; + +3.本示例需要使用DevEco Studio 5.0 才可编译运行。 + +### 下载 +如需单独下载本工程,执行如下命令: +```text +git init +git config core.sparsecheckout true +echo code/BasicFeature/Media/AVCodec/ > .git/info/sparse-checkout +git remote add origin https://gitee.com/openharmony/applications_app_samples.git +git pull origin master +``` diff --git a/code/BasicFeature/Media/AVCodec/build-profile.json5 b/code/BasicFeature/Media/AVCodec/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..11baa04c9cac9badc9318281cdebf824383787ab --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/build-profile.json5 @@ -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. + */ + +{ + "app": { + "signingConfigs": [ + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compileSdkVersion": 13, + "compatibleSdkVersion": 13, + //指定HarmonyOS应用/服务兼容的最低版本。注意使用英文.和() + "targetSdkVersion": 13, + //指定HarmonyOS应用/服务目标版本。若没有设置,默认为compatibleSdkVersion + "runtimeOS": "OpenHarmony", + //指定为HarmonyOS + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/.gitignore b/code/BasicFeature/Media/AVCodec/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/build-profile.json5 b/code/BasicFeature/Media/AVCodec/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..52be05d874aebc88521e3420513e878adb69fbdd --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/build-profile.json5 @@ -0,0 +1,35 @@ +/* + * 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": { + "abiFilters": ["arm64-v8a", "x86_64", "armeabi-v7a"], + "path": "./src/main/cpp/CMakeLists.txt", + "arguments": "", + "cppFlags": "", + } + }, + "targets": [ + { + "name": "default", + "runtimeOS": "OpenHarmony" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/hvigorfile.ts b/code/BasicFeature/Media/AVCodec/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..4d1f1d423fecb1d23d9db5d82759899f44227bdb --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/hvigorfile.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { hapTasks } from '@ohos/hvigor-ohos-plugin'; diff --git a/code/BasicFeature/Media/AVCodec/entry/oh-package.json5 b/code/BasicFeature/Media/AVCodec/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..d4e4debb1ee628315bf76e638d49c5b8283c4e6a --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/oh-package.json5 @@ -0,0 +1,28 @@ +/* + * 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. + */ + +{ + "license": "", + "devDependencies": {}, + "author": "", + "name": "entry", + "description": "Please describe the basic information.", + "main": "", + "version": "1.0.0", + "dependencies": { + "libplayer.so": "file:./src/main/cpp/types/libplayer", + "librecorder.so": "file:./src/main/cpp/types/librecorder" + } +} diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/CMakeLists.txt b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..5cfaabf41fcc2d48176bb2eb9d661392df346495 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/CMakeLists.txt @@ -0,0 +1,41 @@ +# 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}/render/include + ${NATIVERENDER_ROOT_PATH}/sample/player + ${NATIVERENDER_ROOT_PATH}/sample/recorder +) + +set(BASE_LIBRARY + libace_napi.z.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/video_decoder.cpp + capbilities/audio_decoder.cpp + render/plugin_render.cpp + render/plugin_manager.cpp + common/sample_callback.cpp +) + +add_library(recorder SHARED sample/recorder/RecorderNative.cpp + sample/recorder/Recorder.cpp + capbilities/muxer.cpp + capbilities/video_encoder.cpp + common/sample_callback.cpp +) + +target_link_libraries(player PUBLIC ${BASE_LIBRARY}) +target_link_libraries(recorder PUBLIC ${BASE_LIBRARY}) \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/audio_decoder.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/audio_decoder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e27afaf73ae479f74288da1ef0e4d1b3de82104a --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/audio_decoder.cpp @@ -0,0 +1,146 @@ +/* + * 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 "audio_decoder.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); // SAMPLE_S16LE SAMPLE_F32P + 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 ======"); + if (ret != AV_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Config failed, ret: %{public}d", ret); + OH_AVFormat_Destroy(format); + format = nullptr; + return AVCODEC_SAMPLE_ERR_ERROR; + } + OH_AVFormat_Destroy(format); + format = nullptr; + + 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; +} diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/demuxer.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/demuxer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e765d96f6af94b70c14f877f7d23ce5a74dede2b --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/demuxer.cpp @@ -0,0 +1,181 @@ +/* + * 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" +#include + +#undef LOG_TAG +#define LOG_TAG "Demuxer" + +namespace { +using namespace std; +} + +Demuxer::~Demuxer() { Release(); } + +int32_t Demuxer::Create(SampleInfo &info) +{ + /** + * // Need request Internet Permission first in module.json. + * const char *url = "https://hd.ijycnd.com/play/Ddw1W2Ra/index.m3u8"; + * source_ = OH_AVSource_CreateWithURI(const_cast(url)); + */ + 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++) { + auto trackFormat = GetTrackFormat(index); + int trackType = GetTrackType(trackFormat); + if (trackType == MEDIA_TYPE_VID) { + ProcessVideoTrack(trackFormat, index, info); + } else if (trackType == MEDIA_TYPE_AUD) { + ProcessAudioTrack(trackFormat, index, info); + } + } + return AVCODEC_SAMPLE_ERR_OK; +} + +std::shared_ptr Demuxer::GetTrackFormat(int32_t index) +{ + return std::shared_ptr(OH_AVSource_GetTrackFormat(source_, index), OH_AVFormat_Destroy); +} + +int Demuxer::GetTrackType(std::shared_ptr trackFormat) +{ + int trackType = -1; + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_TRACK_TYPE, &trackType); + return trackType; +} + +void Demuxer::ProcessVideoTrack(std::shared_ptr trackFormat, int32_t index, SampleInfo &info) +{ + 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; + + LogVideoConfig(info, videoCodecMime); +} + +void Demuxer::ProcessAudioTrack(std::shared_ptr trackFormat, int32_t index, SampleInfo &info) +{ + 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)); + + HandleCodecConfig(trackFormat, info); + + OH_AVFormat_GetIntValue(trackFormat.get(), OH_MD_KEY_AAC_IS_ADTS, &info.aacAdts); + info.audioCodecMime = audioCodecMime; + audioTrackId_ = index; + + LogAudioConfig(info, audioCodecMime); +} + +void Demuxer::HandleCodecConfig(std::shared_ptr trackFormat, SampleInfo &info) +{ + uint8_t *codecConfig = nullptr; + OH_AVFormat_GetBuffer(trackFormat.get(), OH_MD_KEY_CODEC_CONFIG, &codecConfig, &info.codecConfigLen); + + if (info.codecConfig != nullptr && info.codecConfigLen > 0 && info.codecConfigLen < sizeof(info.codecConfig)) { + copy(codecConfig, codecConfig + info.codecConfigLen, info.codecConfig); + LogCodecConfigDetails(info); + } +} + +void Demuxer::LogVideoConfig(const SampleInfo &info, const char *videoCodecMime) +{ + 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 ======"); +} + +void Demuxer::LogAudioConfig(const SampleInfo &info, const char *audioCodecMime) +{ + AVCODEC_SAMPLE_LOGI("====== Demuxer Audio config ======"); + AVCODEC_SAMPLE_LOGI("audioMime:%{public}s sampleForamt:%{public}d sampleRate:%{public}d " + "channelCount:%{public}d channelLayout:%{public}ld adts:%{public}i", + audioCodecMime, info.audioSampleForamt, info.audioSampleRate, + info.audioChannelCount, info.audioChannelLayout, info.aacAdts); + AVCODEC_SAMPLE_LOGI("====== Demuxer Audio config ======"); +} + +void Demuxer::LogCodecConfigDetails(const SampleInfo &info) +{ + AVCODEC_SAMPLE_LOGI("codecConfig:%{public}p, len:%{public}i, 0:0x%{public}02x 1:0x:%{public}02x, bufLen:%{public}u", + info.codecConfig, static_cast(info.codecConfigLen), + info.codecConfig[0], info.codecConfig[1], + static_cast(sizeof(info.codecConfig))); +} + +int32_t Demuxer::GetVideoTrackId() { return videoTrackId_; } +int32_t Demuxer::GetAudioTrackId() { return audioTrackId_; } \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/audio_decoder.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/audio_decoder.h new file mode 100644 index 0000000000000000000000000000000000000000..0716ea097abb0bb5fdbd34fd92914b895c2419ee --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/audio_decoder.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 AUDIODECODER_H +#define AUDIODECODER_H + +#include "multimedia/player_framework/native_avcodec_audiocodec.h" +#include "multimedia/player_framework/native_avbuffer_info.h" +#include "sample_callback.h" +#include "dfx/error/av_codec_sample_error.h" +#include "av_codec_sample_log.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/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/demuxer.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/demuxer.h new file mode 100644 index 0000000000000000000000000000000000000000..e02347414cbdfe58db0efcca59f8ad33b8f4dac0 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/demuxer.h @@ -0,0 +1,55 @@ +/* + * 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 +#include "napi/native_api.h" +#include "multimedia/player_framework/native_avdemuxer.h" +#include "sample_info.h" +#include "dfx/error/av_codec_sample_error.h" +#include "av_codec_sample_log.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); + std::shared_ptr GetTrackFormat(int32_t index); + int GetTrackType(std::shared_ptr trackFormat); + void ProcessVideoTrack(std::shared_ptr trackFormat, int32_t index, SampleInfo &info); + void ProcessAudioTrack(std::shared_ptr trackFormat, int32_t index, SampleInfo &info); + void HandleCodecConfig(std::shared_ptr trackFormat, SampleInfo &info); + void LogVideoConfig(const SampleInfo &info, const char *videoCodecMime); + void LogAudioConfig(const SampleInfo &info, const char *audioCodecMime); + void LogCodecConfigDetails(const 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/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/muxer.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/muxer.h new file mode 100644 index 0000000000000000000000000000000000000000..b623dcc0402c2f991f9ce04f4fc259bf1b261005 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/muxer.h @@ -0,0 +1,42 @@ +/* + * 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 +#include "multimedia/player_framework/native_avmuxer.h" +#include "sample_info.h" +#include "dfx/error/av_codec_sample_error.h" +#include "av_codec_sample_log.h" + +class Muxer { +public: + Muxer() = default; + ~Muxer(); + + int32_t Create(int32_t fd); + int32_t Config(SampleInfo &sampleInfo); + int32_t Start(); + int32_t WriteSample(OH_AVBuffer *buffer, OH_AVCodecBufferAttr &attr); + int32_t Stop(); + int32_t Release(); + +private: + OH_AVMuxer *muxer_ = nullptr; + int32_t videoTrackId_ = -1; +}; + +#endif // MUXER_H \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/video_decoder.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/video_decoder.h new file mode 100644 index 0000000000000000000000000000000000000000..6ec7b39ca789738efe5a5c3ad37f844e5942e688 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/video_decoder.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 VIDEODECODER_H +#define VIDEODECODER_H + +#include "multimedia/player_framework/native_avcodec_videodecoder.h" +#include "multimedia/player_framework/native_avcapability.h" +#include "multimedia/player_framework/native_avbuffer_info.h" +#include "sample_info.h" +#include "sample_callback.h" +#include "dfx/error/av_codec_sample_error.h" +#include "av_codec_sample_log.h" + +class VideoDecoder { +public: + VideoDecoder() = default; + ~VideoDecoder(); + + int32_t Create(const std::string &videoCodecMime, int32_t videoDecoderType); + int32_t Config(const SampleInfo &sampleInfo, CodecUserData *codecUserData); + int32_t PushInputBuffer(CodecBufferInfo &info); + int32_t FreeOutputBuffer(uint32_t bufferIndex, bool render); + int32_t FreeOutputBuffer(uint32_t bufferIndex, bool render, int64_t timeStamp); + int32_t Start(); + int32_t Release(); + +private: + int32_t SetCallback(CodecUserData *codecUserData); + int32_t Configure(const SampleInfo &sampleInfo); + OH_AVCodec *GetCodecByCategory(const char *mime, bool isEncoder, OH_AVCodecCategory category); + + bool isAVBufferMode_ = false; + OH_AVCodec *decoder_; +}; +#endif // VIDEODECODER_H \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/video_encoder.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/video_encoder.h new file mode 100644 index 0000000000000000000000000000000000000000..0a2a13d1d25c7fcd5ed5b950a95623068ef0ce85 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/include/video_encoder.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 "sample_info.h" +#include "native_window/external_window.h" +#include "native_window/buffer_handle.h" +#include "sample_callback.h" +#include "dfx/error/av_codec_sample_error.h" +#include "av_codec_sample_log.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 PushInputBuffer(CodecBufferInfo &info); + int32_t FreeOutputBuffer(uint32_t bufferIndex); + 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/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/muxer.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/muxer.cpp new file mode 100644 index 0000000000000000000000000000000000000000..959d9a745e281ba59a7c8b384235073e734abbb8 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/muxer.cpp @@ -0,0 +1,105 @@ +/* + * 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" + +#undef LOG_TAG +#define LOG_TAG "Muxer" + +namespace { +constexpr int32_t VERTICAL_ANGLE = 90; +constexpr int32_t HORIZONTAL_ANGLE = 0; +} + +Muxer::~Muxer() +{ + Release(); +} + +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 *formatVideo = OH_AVFormat_CreateVideoFormat(sampleInfo.videoCodecMime.data(), + sampleInfo.videoWidth, sampleInfo.videoHeight); + CHECK_AND_RETURN_RET_LOG(formatVideo != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create video format failed"); + + OH_AVFormat_SetDoubleValue(formatVideo, OH_MD_KEY_FRAME_RATE, sampleInfo.frameRate); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_WIDTH, sampleInfo.videoWidth); + OH_AVFormat_SetIntValue(formatVideo, OH_MD_KEY_HEIGHT, sampleInfo.videoHeight); + OH_AVFormat_SetStringValue(formatVideo, OH_MD_KEY_CODEC_MIME, sampleInfo.videoCodecMime.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); + // 由于相机只有1920×1080的profile,没有1080×1920的profile,所以得往文件里封装一个90度的角度信息,后续播放才会是竖屏显示。 + OH_AVMuxer_SetRotation(muxer_, 90); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "AddTrack failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +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(OH_AVBuffer *buffer, OH_AVCodecBufferAttr &attr) +{ + 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_, videoTrackId_, 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::Stop() +{ + CHECK_AND_RETURN_RET_LOG(muxer_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Muxer is null"); + + int32_t ret = OH_AVMuxer_Stop(muxer_); + CHECK_AND_RETURN_RET_LOG(ret == AV_ERR_OK, AVCODEC_SAMPLE_ERR_ERROR, "Muxer stop failed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Muxer::Release() +{ + if (muxer_ != nullptr) { + OH_AVMuxer_Destroy(muxer_); + muxer_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} + diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/video_decoder.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/video_decoder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..44b669bc26910788fec85bfe9d8eb191a2031725 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/video_decoder.cpp @@ -0,0 +1,188 @@ +/* + * 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 "video_decoder.h" + +#undef LOG_TAG +#define LOG_TAG "VideoDecoder" + +namespace { +constexpr int LIMIT_LOGD_FREQUENCY = 50; +constexpr int ROTATION_ANGLE = 90; +} // namespace + +VideoDecoder::~VideoDecoder() { Release(); } + +OH_AVCodec *VideoDecoder::GetCodecByCategory(const char *mime, bool isEncoder, OH_AVCodecCategory category) +{ + OH_AVCapability *capability = OH_AVCodec_GetCapabilityByCategory(mime, isEncoder, category); + CHECK_AND_RETURN_RET_LOG(capability != nullptr, nullptr, "Capability is nullptr"); + const char *codecName = OH_AVCapability_GetName(capability); + return OH_VideoDecoder_CreateByName(codecName); +} + +int32_t VideoDecoder::Create(const std::string &videoCodecMime, int32_t videoDecoderType) +{ + switch (videoDecoderType) { + case AUTO: + decoder_ = OH_VideoDecoder_CreateByMime(videoCodecMime.c_str()); + CHECK_AND_RETURN_RET_LOG(decoder_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, "Create failed"); + break; + case VIDEO_HW_DECODER: + if (!strcmp(videoCodecMime.data(), "video/avc")) { + decoder_ = GetCodecByCategory(OH_AVCODEC_MIMETYPE_VIDEO_AVC, false, HARDWARE); + } else if (!strcmp(videoCodecMime.data(), "video/hevc")) { + decoder_ = GetCodecByCategory(OH_AVCODEC_MIMETYPE_VIDEO_HEVC, false, HARDWARE); + } else if (!strcmp(videoCodecMime.data(), "video/vvc")) { + decoder_ = GetCodecByCategory(OH_AVCODEC_MIMETYPE_VIDEO_VVC, false, HARDWARE); + } else { + AVCODEC_SAMPLE_LOGE("INVALID MIMETYPE"); + return AVCODEC_SAMPLE_ERR_ERROR; + } + break; + case VIDEO_SW_DECODER: + if (!strcmp(videoCodecMime.data(), "video/avc")) { + decoder_ = GetCodecByCategory(OH_AVCODEC_MIMETYPE_VIDEO_AVC, false, SOFTWARE); + } else if (!strcmp(videoCodecMime.data(), "video/hevc")) { + decoder_ = GetCodecByCategory(OH_AVCODEC_MIMETYPE_VIDEO_HEVC, false, SOFTWARE); + } else if (!strcmp(videoCodecMime.data(), "video/vvc")) { + decoder_ = GetCodecByCategory(OH_AVCODEC_MIMETYPE_VIDEO_VVC, false, SOFTWARE); + } else { + AVCODEC_SAMPLE_LOGE("INVALID MIMETYPE"); + return AVCODEC_SAMPLE_ERR_ERROR; + } + break; + } + return AVCODEC_SAMPLE_ERR_OK; +} + +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) +{ + 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 ======"); + + int ret = OH_VideoDecoder_Configure(decoder_, format); + 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 VideoDecoder::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 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; +} + +int32_t VideoDecoder::FreeOutputBuffer(uint32_t bufferIndex, bool render, int64_t timeStamp) +{ + 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_RenderOutputBufferAtTime(decoder_, bufferIndex, timeStamp); + } 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; +} + +int32_t VideoDecoder::Release() +{ + if (decoder_ != nullptr) { + OH_VideoDecoder_Destroy(decoder_); + decoder_ = nullptr; + } + return AVCODEC_SAMPLE_ERR_OK; +} diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/video_encoder.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/video_encoder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2c27c82445328bd749bc5b6f782976f0998d4093 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/capbilities/video_encoder.cpp @@ -0,0 +1,191 @@ +/* + * 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 "video_encoder.h" + +#undef LOG_TAG +#define LOG_TAG "VideoEncoder" + +namespace { + +int32_t ToGraphicPixelFormat(int32_t avPixelFormat, bool isHDRVivid) +{ + if (isHDRVivid) { + return NATIVEBUFFER_PIXEL_FMT_YCBCR_P010; + } + switch (avPixelFormat) { + case AV_PIXEL_FORMAT_RGBA: + return NATIVEBUFFER_PIXEL_FMT_RGBA_8888; + case AV_PIXEL_FORMAT_YUVI420: + return NATIVEBUFFER_PIXEL_FMT_YCBCR_420_P; + case AV_PIXEL_FORMAT_NV21: + return NATIVEBUFFER_PIXEL_FMT_YCRCB_420_SP; + default: // NV12 and others + return NATIVEBUFFER_PIXEL_FMT_YCRCB_420_SP; + } +} +} // namespace + +VideoEncoder::~VideoEncoder() +{ + Release(); +} + +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; +} + +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; +} + +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; +} + +int32_t VideoEncoder::PushInputBuffer(CodecBufferInfo &info) +{ + CHECK_AND_RETURN_RET_LOG(encoder_ != 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_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; +} + +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; +} + +int32_t VideoEncoder::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_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); + 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); + } + 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 ======"); + + int ret = OH_VideoEncoder_Configure(encoder_, format); + 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 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; +} + diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/dfx/error/av_codec_sample_error.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/dfx/error/av_codec_sample_error.h new file mode 100644 index 0000000000000000000000000000000000000000..258ec92c243b0382f1b483671bee96f3a26856e5 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/dfx/error/av_codec_sample_error.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/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/dfx/log/av_codec_sample_log.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/dfx/log/av_codec_sample_log.h new file mode 100644 index 0000000000000000000000000000000000000000..124aea62d4483bb882f6592b98667b316f544f2c --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/dfx/log/av_codec_sample_log.h @@ -0,0 +1,81 @@ +/* + * 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/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_callback.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_callback.cpp new file mode 100644 index 0000000000000000000000000000000000000000..27e53c53dfca44bb5efb9172e87b700c68d968de --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_callback.cpp @@ -0,0 +1,140 @@ +/* + * 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 "sample_callback.h" +#include "av_codec_sample_log.h" + +namespace { +constexpr int LIMIT_LOGD_FREQUENCY = 50; +constexpr int32_t BYTES_PER_SAMPLE_2 = 2; +} // namespace + +// 自定义写入数据函数 +int32_t SampleCallback::OnRenderWriteData(OH_AudioRenderer *renderer, void *userData, void *buffer, int32_t length) +{ + (void)renderer; + (void)length; + CodecUserData *codecUserData = static_cast(userData); + + // 将待播放的数据,按length长度写入buffer + uint8_t *dest = (uint8_t *)buffer; + size_t index = 0; + std::unique_lock lock(codecUserData->outputMutex); + // 从队列中取出需要播放的长度为length的数据 + 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, codecUserData->renderQueue.size(), index); + + codecUserData->frameWrittenForSpeed += + length / codecUserData->speed / codecUserData->sampleInfo->audioChannelCount / BYTES_PER_SAMPLE_2; + codecUserData->currentPosAudioBufferPts = + codecUserData->endPosAudioBufferPts - codecUserData->renderQueue.size() / + codecUserData->sampleInfo->audioSampleRate / + codecUserData->sampleInfo->audioChannelCount / BYTES_PER_SAMPLE_2; + + if (codecUserData->renderQueue.size() < length) { + codecUserData->renderCond.notify_all(); + } + return 0; +} +// 自定义音频流事件函数 +int32_t SampleCallback::OnRenderStreamEvent(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Event event) +{ + (void)renderer; + (void)userData; + (void)event; + // 根据event表示的音频流事件信息,更新播放器状态和界面 + return 0; +} +// 自定义音频中断事件函数 +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; + // 根据type和hint表示的音频中断信息,更新播放器状态和界面 + return 0; +} +// 自定义异常回调函数 +int32_t SampleCallback::OnRenderError(OH_AudioRenderer *renderer, void *userData, OH_AudioStream_Result error) +{ + (void)renderer; + (void)userData; + (void)error; + AVCODEC_SAMPLE_LOGE("OnRenderError"); + // 根据error表示的音频异常信息,做出相应的处理 + return 0; +} + +void SampleCallback::OnCodecError(OH_AVCodec *codec, int32_t errorCode, void *userData) +{ + (void)codec; + (void)errorCode; + (void)userData; + AVCODEC_SAMPLE_LOGE("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); + std::unique_lock lock(codecUserData->inputMutex); + if (codecUserData->isEncFirstFrame) { + OH_AVFormat *format = OH_VideoEncoder_GetInputDescription(codec); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_WIDTH, &codecUserData->width); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_PIC_HEIGHT, &codecUserData->height); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_STRIDE, &codecUserData->widthStride); + OH_AVFormat_GetIntValue(format, OH_MD_KEY_VIDEO_SLICE_HEIGHT, &codecUserData->heightStride); + OH_AVFormat_Destroy(format); + codecUserData->isEncFirstFrame = false; + } + codecUserData->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); + std::unique_lock lock(codecUserData->outputMutex); + 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; + } + codecUserData->outputBufferInfoQueue.emplace(index, buffer); + codecUserData->outputCond.notify_all(); +} diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_callback.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_callback.h new file mode 100644 index 0000000000000000000000000000000000000000..f1e1f60b2e7a49ab81102321ac324a81d43291a6 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_callback.h @@ -0,0 +1,37 @@ +/* + * 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 +#include "sample_info.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); +}; + +#endif // AVCODEC_SAMPLE_CALLBACK_H diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_info.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_info.h new file mode 100644 index 0000000000000000000000000000000000000000..79b22251ae9c422e061093c5a30cded28a69ddca --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/common/sample_info.h @@ -0,0 +1,160 @@ +/* + * 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 +#include +#include +#include "multimedia/player_framework/native_avcodec_base.h" +#include "multimedia/player_framework/native_avbuffer.h" + +using namespace std; + +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 + +const unordered_map PIXEL_FORMAT_TO_STRING = { + {AV_PIXEL_FORMAT_YUVI420, "YUVI420"}, + {AV_PIXEL_FORMAT_NV12, "NV12"}, + {AV_PIXEL_FORMAT_NV21, "NV21"}, + {AV_PIXEL_FORMAT_SURFACE_FORMAT, "SURFACE_FORMAT"}, + {AV_PIXEL_FORMAT_RGBA, "RGBA"}, +}; + +struct SampleInfo { + int32_t inputFd = -1; + int32_t outputFd = -1; + int64_t inputFileOffset = 0; + int64_t inputFileSize = 0; + string inputFilePath; + string videoCodecMime = ""; + string audioCodecMime = ""; + int32_t videoWidth = 0; + int32_t videoHeight = 0; + 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 codecType = 0; + int32_t codecRunMode = 0; + string outputFilePath; + + int32_t audioSampleForamt = 0; + int32_t audioSampleRate = 0; + int32_t audioChannelCount = 0; + int64_t audioChannelLayout = 0; + + int32_t isHDRVivid = 0; + int32_t hevcProfile = HEVC_PROFILE_MAIN; + OH_ColorPrimary primary = COLOR_PRIMARY_BT2020; + OH_TransferCharacteristic transfer = TRANSFER_CHARACTERISTIC_HLG; + 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); + }; +}; + +enum CodecType { + AUTO = 0, + VIDEO_HW_DECODER = 1, + VIDEO_SW_DECODER = 2, + VIDEO_HW_ENCODER = 3, + VIDEO_SW_ENCODER = 4, +}; + +enum CodecRunMode { + SURFACE = 0, + BUFFER = 1 +}; + +struct CodecUserData { +public: + SampleInfo *sampleInfo = nullptr; + bool isDecFirstFrame = false; + bool isEncFirstFrame = false; + + int32_t width = 0; + int32_t height = 0; + int32_t widthStride = 0; + int32_t heightStride = 0; + + uint32_t inputFrameCount = 0; + mutex inputMutex; + condition_variable inputCond; + queue inputBufferInfoQueue; + + uint32_t outputFrameCount = 0; + mutex outputMutex; + condition_variable outputCond; + mutex renderMutex; + condition_variable renderCond; + queue outputBufferInfoQueue; + + queue renderQueue; + + int64_t speed = 1.0f; + int64_t frameWrittenForSpeed = 0; + int64_t endPosAudioBufferPts = 0; + int64_t currentPosAudioBufferPts = 0; + + void ClearQueue() + { + { + unique_lock lock(inputMutex); + auto emptyQueue = queue(); + inputBufferInfoQueue.swap(emptyQueue); + } + { + unique_lock lock(outputMutex); + auto emptyQueue = queue(); + outputBufferInfoQueue.swap(emptyQueue); + } + } +}; + +#endif // AVCODEC_SAMPLE_INFO_H \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/include/plugin_manager.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/include/plugin_manager.h new file mode 100644 index 0000000000000000000000000000000000000000..a440094bbcf844fb4ea14501d3ad8b2ec2b9c45d --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/include/plugin_manager.h @@ -0,0 +1,53 @@ +/* + * 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 NATIVE_XCOMPONENT_PLUGIN_MANAGER_H +#define NATIVE_XCOMPONENT_PLUGIN_MANAGER_H + +#include +#include +#include +#include +#include +#include +#include "native_window/external_window.h" + +#include "plugin_render.h" + +namespace NativeXComponentSample { +class PluginManager { +public: + ~PluginManager(); + + static PluginManager* GetInstance() + { + return &PluginManager::pluginManager_; + } + + static napi_value GetContext(napi_env env, napi_callback_info info); + + void SetNativeXComponent(std::string& id, OH_NativeXComponent* nativeXComponent); + PluginRender* GetRender(std::string& id); + void Export(napi_env env, napi_value exports); + OHNativeWindow *pluginWindow_; + +private: + static PluginManager pluginManager_; + + std::unordered_map nativeXComponentMap_; + std::unordered_map pluginRenderMap_; +}; +} // namespace NativeXComponentSample +#endif // NATIVE_XCOMPONENT_PLUGIN_MANAGER_H diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/include/plugin_render.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/include/plugin_render.h new file mode 100644 index 0000000000000000000000000000000000000000..53fabbfdc65942ad12b16f8bbc81a1600b668f09 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/include/plugin_render.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 NATIVE_XCOMPONENT_PLUGIN_RENDER_H +#define NATIVE_XCOMPONENT_PLUGIN_RENDER_H + +#include +#include +#include +#include + +namespace NativeXComponentSample { +class PluginRender { +public: + explicit PluginRender(std::string& id); + ~PluginRender() {} + static PluginRender* GetInstance(std::string& id); + static void Release(std::string& id); + void Export(napi_env env, napi_value exports); + void OnSurfaceChanged(OH_NativeXComponent* component, void* window); + void OnTouchEvent(OH_NativeXComponent* component, void* window); + void RegisterCallback(OH_NativeXComponent* nativeXComponent); + +public: + static std::unordered_map instance_; + std::string id_; + +private: + OH_NativeXComponent_Callback renderCallback_; +}; +} // namespace NativeXComponentSample +#endif // NATIVE_XCOMPONENT_PLUGIN_RENDER_H diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/plugin_manager.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/plugin_manager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5895e9633a46169d2605d21589eb8afa665061e8 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/plugin_manager.cpp @@ -0,0 +1,163 @@ +/* + * 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 "plugin_manager.h" + +#include +#include +#include +#include +#include + +#undef LOG_TAG +#define LOG_TAG "PLUGINMANAGER" + +namespace NativeXComponentSample { +constexpr uint32_t LOG_PRINT_DOMAIN = 0xFF00; +PluginManager PluginManager::pluginManager_; + +PluginManager::~PluginManager() +{ + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "~PluginManager"); + for (auto iter = nativeXComponentMap_.begin(); iter != nativeXComponentMap_.end(); ++iter) { + if (iter->second != nullptr) { + delete iter->second; + iter->second = nullptr; + } + } + nativeXComponentMap_.clear(); + + for (auto iter = pluginRenderMap_.begin(); iter != pluginRenderMap_.end(); ++iter) { + if (iter->second != nullptr) { + delete iter->second; + iter->second = nullptr; + } + } + pluginRenderMap_.clear(); +} + +napi_value PluginManager::GetContext(napi_env env, napi_callback_info info) +{ + if ((env == nullptr) || (info == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager", "GetContext env or info is null"); + return nullptr; + } + + size_t argCnt = 1; + napi_value args[1] = { nullptr }; + if (napi_get_cb_info(env, info, &argCnt, args, nullptr, nullptr) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager", "GetContext napi_get_cb_info failed"); + } + + if (argCnt != 1) { + napi_throw_type_error(env, NULL, "Wrong number of arguments"); + return nullptr; + } + + napi_valuetype valuetype; + if (napi_typeof(env, args[0], &valuetype) != napi_ok) { + napi_throw_type_error(env, NULL, "napi_typeof failed"); + return nullptr; + } + + if (valuetype != napi_number) { + napi_throw_type_error(env, NULL, "Wrong type of arguments"); + return nullptr; + } + + int64_t value; + if (napi_get_value_int64(env, args[0], &value) != napi_ok) { + napi_throw_type_error(env, NULL, "napi_get_value_int64 failed"); + return nullptr; + } + + napi_value exports; + if (napi_create_object(env, &exports) != napi_ok) { + napi_throw_type_error(env, NULL, "napi_create_object failed"); + return nullptr; + } + + return exports; +} + +void PluginManager::Export(napi_env env, napi_value exports) +{ + if ((env == nullptr) || (exports == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager", "Export: env or exports is null"); + return; + } + + napi_value exportInstance = nullptr; + if (napi_get_named_property(env, exports, OH_NATIVE_XCOMPONENT_OBJ, &exportInstance) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager", "Export: napi_get_named_property fail"); + return; + } + + OH_NativeXComponent* nativeXComponent = nullptr; + if (napi_unwrap(env, exportInstance, reinterpret_cast(&nativeXComponent)) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager", "Export: napi_unwrap fail"); + return; + } + + char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = { '\0' }; + uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; + if (OH_NativeXComponent_GetXComponentId(nativeXComponent, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginManager", "Export: OH_NativeXComponent_GetXComponentId fail"); + return; + } + + std::string id(idStr); + auto context = PluginManager::GetInstance(); + if ((context != nullptr) && (nativeXComponent != nullptr)) { + context->SetNativeXComponent(id, nativeXComponent); + auto render = context->GetRender(id); + if (render != nullptr) { + render->RegisterCallback(nativeXComponent); + render->Export(env, exports); + } + } +} + +void PluginManager::SetNativeXComponent(std::string& id, OH_NativeXComponent* nativeXComponent) +{ + if (nativeXComponent == nullptr) { + return; + } + + if (nativeXComponentMap_.find(id) == nativeXComponentMap_.end()) { + nativeXComponentMap_[id] = nativeXComponent; + return; + } + + if (nativeXComponentMap_[id] != nativeXComponent) { + OH_NativeXComponent* tmp = nativeXComponentMap_[id]; + delete tmp; + tmp = nullptr; + nativeXComponentMap_[id] = nativeXComponent; + } +} + +PluginRender* PluginManager::GetRender(std::string& id) +{ + if (pluginRenderMap_.find(id) == pluginRenderMap_.end()) { + PluginRender* instance = PluginRender::GetInstance(id); + pluginRenderMap_[id] = instance; + return instance; + } + + return pluginRenderMap_[id]; +} +} // namespace NativeXComponentSample diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/plugin_render.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/plugin_render.cpp new file mode 100644 index 0000000000000000000000000000000000000000..57912cc5d6ed8ecf323426964d75cee07f7fa918 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/render/plugin_render.cpp @@ -0,0 +1,223 @@ +/* + * 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 +#include +#include +#include +#include + +#include "plugin_manager.h" +#include "plugin_render.h" + +#undef LOG_TAG +#define LOG_TAG "PLUGINRENDER" + +namespace NativeXComponentSample { +namespace { +constexpr uint32_t LOG_PRINT_DOMAIN = 0xFF00; + +void OnSurfaceCreatedCB(OH_NativeXComponent* component, void* window) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceCreatedCB"); + if ((component == nullptr) || (window == nullptr)) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceCreatedCB: component or window is null"); + return; + } + + char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = { '\0' }; + uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; + if (OH_NativeXComponent_GetXComponentId(component, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceCreatedCB: Unable to get XComponent id"); + return; + } + + std::string id(idStr); + auto render = PluginRender::GetInstance(id); + uint64_t width; + uint64_t height; + int32_t xSize = OH_NativeXComponent_GetXComponentSize(component, window, &width, &height); + if ((xSize == OH_NATIVEXCOMPONENT_RESULT_SUCCESS) && (render != nullptr)) { + auto context = PluginManager::GetInstance(); + context->pluginWindow_ = (OHNativeWindow *)window; + OH_NativeWindow_NativeWindowSetScalingModeV2(context->pluginWindow_, OH_SCALING_MODE_SCALE_FIT_V2); + } +} + +void OnSurfaceChangedCB(OH_NativeXComponent* component, void* window) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceChangedCB"); + if ((component == nullptr) || (window == nullptr)) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceChangedCB: component or window is null"); + return; + } + + char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = { '\0' }; + uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; + if (OH_NativeXComponent_GetXComponentId(component, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceChangedCB: Unable to get XComponent id"); + return; + } + + std::string id(idStr); + auto render = PluginRender::GetInstance(id); + if (render != nullptr) { + render->OnSurfaceChanged(component, window); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Callback", "surface changed"); + } +} + +void OnSurfaceDestroyedCB(OH_NativeXComponent* component, void* window) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceDestroyedCB"); + if ((component == nullptr) || (window == nullptr)) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceDestroyedCB: component or window is null"); + return; + } + + char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = { '\0' }; + uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; + if (OH_NativeXComponent_GetXComponentId(component, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceDestroyedCB: Unable to get XComponent id"); + return; + } + + std::string id(idStr); + PluginRender::Release(id); +} + +void DispatchTouchEventCB(OH_NativeXComponent* component, void* window) +{ + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "Callback", "DispatchTouchEventCB"); + if ((component == nullptr) || (window == nullptr)) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "DispatchTouchEventCB: component or window is null"); + return; + } + + char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = { '\0' }; + uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; + if (OH_NativeXComponent_GetXComponentId(component, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "DispatchTouchEventCB: Unable to get XComponent id"); + return; + } + + std::string id(idStr); + PluginRender* render = PluginRender::GetInstance(id); + if (render != nullptr) { + render->OnTouchEvent(component, window); + } +} +} // namespace + +std::unordered_map PluginRender::instance_; +PluginRender::PluginRender(std::string& id) +{ + this->id_ = id; +} + +PluginRender* PluginRender::GetInstance(std::string& id) +{ + if (instance_.find(id) == instance_.end()) { + PluginRender* instance = new PluginRender(id); + instance_[id] = instance; + return instance; + } else { + return instance_[id]; + } +} + +void PluginRender::Export(napi_env env, napi_value exports) +{ + if ((env == nullptr) || (exports == nullptr)) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginRender", "Export: env or exports is null"); + return; + } + + napi_property_descriptor desc[] = {}; + if (napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc) != napi_ok) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "PluginRender", "Export: napi_define_properties failed"); + } +} + +void PluginRender::Release(std::string& id) +{ + PluginRender* render = PluginRender::GetInstance(id); + if (render != nullptr) { + delete render; + render = nullptr; + instance_.erase(instance_.find(id)); + } +} + +void PluginRender::OnSurfaceChanged(OH_NativeXComponent* component, void* window) +{ + char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = { '\0' }; + uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; + if (OH_NativeXComponent_GetXComponentId(component, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "OnSurfaceChanged: Unable to get XComponent id"); + return; + } + + std::string id(idStr); + PluginRender* render = PluginRender::GetInstance(id); + double offsetX; + double offsetY; + OH_NativeXComponent_GetXComponentOffset(component, window, &offsetX, &offsetY); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "OH_NativeXComponent_GetXComponentOffset", + "offsetX = %{public}lf, offsetY = %{public}lf", offsetX, offsetY); + uint64_t width; + uint64_t height; + OH_NativeXComponent_GetXComponentSize(component, window, &width, &height); +} + +void PluginRender::OnTouchEvent(OH_NativeXComponent* component, void* window) +{ + char idStr[OH_XCOMPONENT_ID_LEN_MAX + 1] = { '\0' }; + uint64_t idSize = OH_XCOMPONENT_ID_LEN_MAX + 1; + if (OH_NativeXComponent_GetXComponentId(component, idStr, &idSize) != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) { + OH_LOG_Print( + LOG_APP, LOG_ERROR, LOG_PRINT_DOMAIN, "Callback", "DispatchTouchEventCB: Unable to get XComponent id"); + return; + } + OH_NativeXComponent_TouchEvent touchEvent; + OH_NativeXComponent_GetTouchEvent(component, window, &touchEvent); + float tiltX = 0.0f; + float tiltY = 0.0f; + OH_NativeXComponent_TouchPointToolType toolType = + OH_NativeXComponent_TouchPointToolType::OH_NATIVEXCOMPONENT_TOOL_TYPE_UNKNOWN; + OH_NativeXComponent_GetTouchPointToolType(component, 0, &toolType); + OH_NativeXComponent_GetTouchPointTiltX(component, 0, &tiltX); + OH_NativeXComponent_GetTouchPointTiltY(component, 0, &tiltY); + OH_LOG_Print(LOG_APP, LOG_INFO, LOG_PRINT_DOMAIN, "OnTouchEvent", + "touch info: toolType = %{public}d, tiltX = %{public}lf, tiltY = %{public}lf", toolType, tiltX, tiltY); +} + +void PluginRender::RegisterCallback(OH_NativeXComponent *nativeXComponent) +{ + renderCallback_.OnSurfaceCreated = OnSurfaceCreatedCB; + renderCallback_.OnSurfaceChanged = OnSurfaceChangedCB; + renderCallback_.OnSurfaceDestroyed = OnSurfaceDestroyedCB; + renderCallback_.DispatchTouchEvent = DispatchTouchEventCB; + OH_NativeXComponent_RegisterCallback(nativeXComponent, &renderCallback_); +} +} // namespace NativeXComponentSample diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/Player.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/Player.cpp new file mode 100644 index 0000000000000000000000000000000000000000..b9a5409931eca8a2a137c5c6bcde81884028720a --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/Player.cpp @@ -0,0 +1,782 @@ +/* + * 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 "av_codec_sample_log.h" +#include "dfx/error/av_codec_sample_error.h" +#include +#include +#include + +#undef LOG_TAG +#define LOG_TAG "samplePlayer" + +namespace { +constexpr int BALANCE_VALUE = 5; +using namespace std::string_literals; +using namespace std::chrono_literals; +static const int MS_TO_S = 1000; +constexpr int64_t WAIT_TIME_US_THRESHOLD_WARNING = -1 * 40 * 1000; // warning threshold 40ms +constexpr int64_t WAIT_TIME_US_THRESHOLD = 1 * 1000 * 1000; // max sleep time 1s +constexpr int64_t SINK_TIME_US_THRESHOLD = 100000; // max sink time 100ms +constexpr int32_t BYTES_PER_SAMPLE_2 = 2; // 2 bytes per sample +constexpr double VSYNC_TIME = 1000 / 60; // frame time +constexpr double LIP_SYNC_BALANCE_VALUE = 2; // the balance value of sync sound and picture +constexpr int8_t YUV420_SAMPLE_RATIO = 2; +constexpr int32_t TRIPLE_SPEED_MULTIPLIER = 3; +constexpr int32_t DOUBLE_SPEED_MULTIPLIER = 2; +constexpr int64_t MICROSECOND_TO_S = 1000000; +constexpr int64_t NANO_TO_S = 1000000000; + +std::string ToString(OH_AVPixelFormat pixelFormat) +{ + std::string ret; + auto iter = PIXEL_FORMAT_TO_STRING.find(pixelFormat); + if (iter != PIXEL_FORMAT_TO_STRING.end()) { + ret = PIXEL_FORMAT_TO_STRING.at(pixelFormat); + } + return ret; +} +} // namespace + +Player::~Player() { Player::StartRelease(); } + +int32_t Player::CreateAudioDecoder() +{ + AVCODEC_SAMPLE_LOGW("audio mime:%{public}s", sampleInfo_.audioCodecMime.c_str()); + int32_t ret = audioDecoder_->Create(sampleInfo_.audioCodecMime); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + isAudioDone.store(true); + AVCODEC_SAMPLE_LOGE("Create audio decoder failed, mime:%{public}s", sampleInfo_.audioCodecMime.c_str()); + } else { + audioDecContext_ = new CodecUserData; + ret = audioDecoder_->Config(sampleInfo_, audioDecContext_); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Audio Decoder config failed"); + OH_AudioStreamBuilder_Create(&builder_, AUDIOSTREAM_TYPE_RENDERER); + OH_AudioStreamBuilder_SetLatencyMode(builder_, AUDIOSTREAM_LATENCY_MODE_NORMAL); + // 设置音频采样率 + OH_AudioStreamBuilder_SetSamplingRate(builder_, sampleInfo_.audioSampleRate); + // 设置音频声道 + OH_AudioStreamBuilder_SetChannelCount(builder_, sampleInfo_.audioChannelCount); + // 设置音频采样格式 + OH_AudioStreamBuilder_SetSampleFormat(builder_, AUDIOSTREAM_SAMPLE_S16LE); + // 设置音频流的编码类型 + OH_AudioStreamBuilder_SetEncodingType(builder_, AUDIOSTREAM_ENCODING_TYPE_RAW); + // 设置输出音频流的工作场景 + OH_AudioStreamBuilder_SetRendererInfo(builder_, AUDIOSTREAM_USAGE_MOVIE); + AVCODEC_SAMPLE_LOGW("Init audioSampleRate: %{public}d, ChannelCount: %{public}d", sampleInfo_.audioSampleRate, + sampleInfo_.audioChannelCount); + OH_AudioRenderer_Callbacks callbacks; + // 配置回调函数 +#ifndef DEBUG_DECODE + callbacks.OH_AudioRenderer_OnWriteData = SampleCallback::OnRenderWriteData; +#else + callbacks.OH_AudioRenderer_OnWriteData = nullptr; +#endif + callbacks.OH_AudioRenderer_OnStreamEvent = SampleCallback::OnRenderStreamEvent; + callbacks.OH_AudioRenderer_OnInterruptEvent = SampleCallback::OnRenderInterruptEvent; + callbacks.OH_AudioRenderer_OnError = SampleCallback::OnRenderError; + // 设置输出音频流的回调 + OH_AudioStreamBuilder_SetRendererCallback(builder_, callbacks, audioDecContext_); + OH_AudioStreamBuilder_GenerateRenderer(builder_, &audioRenderer_); + } + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Player::CreateVideoDecoder() +{ + AVCODEC_SAMPLE_LOGW("video mime:%{public}s", sampleInfo_.videoCodecMime.c_str()); + int32_t ret = videoDecoder_->Create(sampleInfo_.videoCodecMime, sampleInfo_.codecType); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + isVideoDone.store(true); + AVCODEC_SAMPLE_LOGW("Create video decoder failed, mime:%{public}s", sampleInfo_.videoCodecMime.c_str()); + } else { + videoDecContext_ = new CodecUserData; + videoDecContext_->isDecFirstFrame = true; + if (sampleInfo_.codecRunMode == SURFACE) { + sampleInfo_.window = NativeXComponentSample::PluginManager::GetInstance()->pluginWindow_; + } else { + sampleInfo_.window = nullptr; + } + 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::HandleInitError(std::unique_lock& outerLock) +{ + { + std::unique_lock doneLock(doneMutex); + isAudioDone = true; + isVideoDone = true; + isReleased_ = false; + } + doneCond_.notify_all(); + if (outerLock.owns_lock()) { + outerLock.unlock(); + } + StartRelease(); + return AVCODEC_SAMPLE_ERR_ERROR; +} + +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; + + videoDecoder_ = std::make_unique(); + audioDecoder_ = std::make_unique(); + demuxer_ = std::make_unique(); + + int32_t ret = demuxer_->Create(sampleInfo_); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Create demuxer failed"); + return HandleInitError(lock); + } + + ret = CreateAudioDecoder(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Create audio decoder failed"); + return HandleInitError(lock); + } + + ret = CreateVideoDecoder(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Create video decoder failed"); + return HandleInitError(lock); + } + + if (audioDecContext_ != nullptr) { + audioDecContext_->sampleInfo = &sampleInfo_; + } + + isReleased_ = false; + AVCODEC_SAMPLE_LOGI("Succeed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Player::StartVideoDecoder() +{ + CHECK_AND_RETURN_RET_LOG(!videoDecInputThread_ && !videoDecOutputThread_, + AVCODEC_SAMPLE_ERR_ERROR, "Video threads already running"); + + int32_t ret = videoDecoder_->Start(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Video Decoder start failed"); + return ret; + } + + videoDecInputThread_ = std::make_unique(&Player::VideoDecInputThread, this); + videoDecOutputThread_ = std::make_unique(&Player::VideoDecOutputThread, this); + + if (!videoDecInputThread_ || !videoDecOutputThread_) { + AVCODEC_SAMPLE_LOGE("Create video threads failed"); + return AVCODEC_SAMPLE_ERR_ERROR; + } + + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Player::StartAudioDecoder() +{ + CHECK_AND_RETURN_RET_LOG(!audioDecInputThread_ && !audioDecOutputThread_, + AVCODEC_SAMPLE_ERR_ERROR, "Audio threads already running"); + + int32_t ret = audioDecoder_->Start(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("Audio Decoder start failed"); + return ret; + } + + audioDecInputThread_ = std::make_unique(&Player::AudioDecInputThread, this); + audioDecOutputThread_ = std::make_unique(&Player::AudioDecOutputThread, this); + + if (!audioDecInputThread_ || !audioDecOutputThread_) { + AVCODEC_SAMPLE_LOGE("Create audio threads failed"); + return AVCODEC_SAMPLE_ERR_ERROR; + } + +#ifdef DEBUG_DECODE + audioOutputFile_.open("/data/storage/el2/base/haps/entry/files/audio_decode_out.pcm", + std::ios::out | std::ios::binary); +#endif + + // Clear audio render queue + while (audioDecContext_ && !audioDecContext_->renderQueue.empty()) { + audioDecContext_->renderQueue.pop(); + } + + return AVCODEC_SAMPLE_ERR_OK; +} + +void Player::CleanupAfterStartFailure() +{ + StartRelease(); + doneCond_.notify_all(); +} + +int32_t Player::Start() +{ + std::unique_lock lock(mutex_); + + CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started"); + CHECK_AND_RETURN_RET_LOG(demuxer_, AVCODEC_SAMPLE_ERR_ERROR, "Demuxer not initialized"); + + isStarted_ = true; + int32_t ret = AVCODEC_SAMPLE_ERR_OK; + + if (videoDecContext_) { + ret = StartVideoDecoder(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + lock.unlock(); + CleanupAfterStartFailure(); + return ret; + } + } + + if (audioDecContext_) { + ret = StartAudioDecoder(); + if (ret == AVCODEC_SAMPLE_ERR_OK && audioRenderer_) { + OH_AudioRenderer_Start(audioRenderer_); + } + + if (ret != AVCODEC_SAMPLE_ERR_OK) { + lock.unlock(); + CleanupAfterStartFailure(); + return ret; + } + } + + AVCODEC_SAMPLE_LOGI("Player started successfully"); + doneCond_.notify_all(); + return AVCODEC_SAMPLE_ERR_OK; +} + +void Player::SetSpeed(float multiplier) +{ + if (this->speed == multiplier) { + AVCODEC_SAMPLE_LOGE("Same speed value"); + return; + } + if (audioRenderer_) { + OH_AudioRenderer_SetSpeed(audioRenderer_, multiplier); + } + this->speed = multiplier; + if (audioDecContext_) { + audioDecContext_->speed = multiplier; + } +} + +void Player::StartRelease() +{ + AVCODEC_SAMPLE_LOGI("StartRelease"); + std::unique_lock lock(doneMutex); + doneCond_.wait(lock, [this]() { return isAudioDone.load() && isVideoDone.load(); }); + if (audioRenderer_) { + OH_AudioRenderer_Stop(audioRenderer_); + } + if (!isReleased_) { + isReleased_ = true; + Release(); + } +} + +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::Release() +{ + std::lock_guard lock(mutex_); + isStarted_ = false; + isAudioDone = false; + isVideoDone = false; + if (audioRenderer_ != nullptr) { + OH_AudioRenderer_Release(audioRenderer_); + audioRenderer_ = nullptr; + } +#ifdef DEBUG_DECODE + if (audioOutputFile_.is_open()) { + audioOutputFile_.close(); + } +#endif + 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; + } + outputFile_ = nullptr; + if (builder_ != nullptr) { + OH_AudioStreamBuilder_Destroy(builder_); + builder_ = nullptr; + } + doneCond_.notify_all(); + // 触发回调 + sampleInfo_.playDoneCallback(sampleInfo_.playDoneCallbackData); + // 清空队列 + while (audioDecContext_ && !audioDecContext_->renderQueue.empty()) { + audioDecContext_->renderQueue.pop(); + } + AVCODEC_SAMPLE_LOGI("Succeed"); +} + +void Player::DumpOutput(CodecBufferInfo &bufferInfo) +{ + auto &info = sampleInfo_; + if (info.codecRunMode != BUFFER) { + return; + } + if (outputFile_ == nullptr) { + auto time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + // dump file /data/app/el2/100/base/com.samples.avcodecsample/haps/entry/files/haps/entry/files/ + if (info.outputFilePath.empty()) { + info.outputFilePath = "/data/storage/el2/base/haps/entry/files/VideoDecoderOut_"s + + ToString(info.pixelFormat) + "_" + std::to_string(info.videoWidth) + "_" + + std::to_string(info.videoHeight) + "_" + std::to_string(time) + ".yuv"; + } + + outputFile_ = std::make_unique(info.outputFilePath, std::ios::out | std::ios::trunc); + if (!outputFile_->is_open()) { + outputFile_ = nullptr; + AVCODEC_SAMPLE_LOGE("Output file open failed"); + return; + } + } + + uint8_t *bufferAddr = OH_AVBuffer_GetAddr(reinterpret_cast(bufferInfo.buffer)); + CHECK_AND_RETURN_LOG(bufferAddr != nullptr, "Buffer is nullptr"); + switch (info.pixelFormat) { + case AV_PIXEL_FORMAT_YUVI420: + WriteOutputFileWithStrideYUV420P(bufferAddr); + break; + case AV_PIXEL_FORMAT_NV12: + [[fallthrough]]; + case AV_PIXEL_FORMAT_NV21: + WriteOutputFileWithStrideYUV420SP(bufferAddr); + break; + case AV_PIXEL_FORMAT_RGBA: + WriteOutputFileWithStrideRGBA(bufferAddr); + break; + default: + AVCODEC_SAMPLE_LOGE("Unsupported pixel format, skip"); + break; + } +} + +void Player::WriteOutputFileWithStrideYUV420P(uint8_t *bufferAddr) +{ + CHECK_AND_RETURN_LOG(bufferAddr != nullptr, "Buffer is nullptr"); + 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; + + // copy Y + for (int32_t row = 0; row < videoDecContext_->height; row++) { + outputFile_->write(reinterpret_cast(bufferAddr), videoWidth); + bufferAddr += stride; + } + bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) * stride; + + // copy U + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + outputFile_->write(reinterpret_cast(bufferAddr), uvWidth); + bufferAddr += uvStride; + } + bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) / YUV420_SAMPLE_RATIO * uvStride; + // copy V + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + outputFile_->write(reinterpret_cast(bufferAddr), uvWidth); + bufferAddr += uvStride; + } +} + +void Player::WriteOutputFileWithStrideYUV420SP(uint8_t *bufferAddr) +{ + CHECK_AND_RETURN_LOG(bufferAddr != nullptr, "Buffer is nullptr"); + 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; + + // copy Y + for (int32_t row = 0; row < videoDecContext_->height; row++) { + outputFile_->write(reinterpret_cast(bufferAddr), videoWidth); + bufferAddr += stride; + } + bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) * stride; + + // copy U + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + outputFile_->write(reinterpret_cast(bufferAddr), uvWidth); + bufferAddr += uvStride; + } + bufferAddr += (videoDecContext_->heightStride - videoDecContext_->height) / YUV420_SAMPLE_RATIO * uvStride; + // copy V + for (int32_t row = 0; row < (videoDecContext_->height / YUV420_SAMPLE_RATIO); row++) { + outputFile_->write(reinterpret_cast(bufferAddr), uvWidth); + bufferAddr += uvStride; + } +} + +void Player::WriteOutputFileWithStrideRGBA(uint8_t *bufferAddr) +{ + CHECK_AND_RETURN_LOG(bufferAddr != nullptr, "Buffer is nullptr"); + auto &info = sampleInfo_; + int32_t width = + videoDecContext_->width * + ((info.videoCodecMime == OH_AVCODEC_MIMETYPE_VIDEO_HEVC && info.hevcProfile == HEVC_PROFILE_MAIN_10) ? 2 : 1); + int32_t &stride = videoDecContext_->widthStride; + + for (int32_t row = 0; row < videoDecContext_->heightStride; row++) { + outputFile_->write(reinterpret_cast(bufferAddr), width * 4); // 4: RGBA 4 channels + bufferAddr += stride; + } +} + +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), "Catch EOS, thread out"); + } +} + +bool Player::ProcessVideoWithoutAudio(CodecBufferInfo& bufferInfo, + std::chrono::time_point& lastPushTime) +{ + DumpOutput(bufferInfo); + int32_t ret = videoDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, !sampleInfo_.codecRunMode, GetCurrentTime()); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("FreeOutputBuffer failed: %{public}d", ret); + return false; + } + this->speed == 1 ? sampleInfo_.frameInterval = MICROSECOND_TO_S / sampleInfo_.frameRate + : this->speed == DOUBLE_SPEED_MULTIPLIER ? sampleInfo_.frameInterval = + MICROSECOND_TO_S / sampleInfo_.frameRate / DOUBLE_SPEED_MULTIPLIER + : sampleInfo_.frameInterval = + MICROSECOND_TO_S / sampleInfo_.frameRate / TRIPLE_SPEED_MULTIPLIER; + std::this_thread::sleep_until(lastPushTime + std::chrono::microseconds(sampleInfo_.frameInterval)); + lastPushTime = std::chrono::system_clock::now(); + + return true; +} + +bool Player::CalculateSyncParameters(CodecBufferInfo& bufferInfo, int64_t framePosition, + int64_t& waitTimeUs, bool& dropFrame, + int64_t perSinkTimeThreshold) +{ + // after seek, audio render flush, framePosition = 0, then writtenSampleCnt = 0 + int64_t latency = (audioDecContext_->frameWrittenForSpeed - framePosition) * 1000 * 1000 / + sampleInfo_.audioSampleRate / speed; + AVCODEC_SAMPLE_LOGI("VD latency: %{public}li writtenSampleCnt: %{public}li", latency, writtenSampleCnt); + + nowTimeStamp = GetCurrentTime(); + int64_t anchorDiff = (nowTimeStamp - audioTimeStamp) / 1000; + + // us, audio buffer accelerate render time + int64_t audioPlayedTime = audioDecContext_->currentPosAudioBufferPts - latency + anchorDiff; + // us, video buffer expected render time + int64_t videoPlayedTime = bufferInfo.attr.pts; + + // audio render timestamp and now timestamp diff + waitTimeUs = videoPlayedTime - audioPlayedTime; // us + + AVCODEC_SAMPLE_LOGI("VD bufferInfo.bufferIndex: %{public}li", bufferInfo.bufferIndex); + AVCODEC_SAMPLE_LOGI( + "VD audioPlayedTime: %{public}li, videoPlayedTime: %{public}li, nowTimeStamp_:{public}ld, " + "audioTimeStamp_ :{public}ld, waitTimeUs :{public}ld, anchordiff :%{public}ld", + audioPlayedTime, videoPlayedTime, nowTimeStamp, audioTimeStamp, waitTimeUs, anchorDiff); + dropFrame = false; + // video buffer is too late, drop it + if (waitTimeUs < WAIT_TIME_US_THRESHOLD_WARNING) { + dropFrame = true; + AVCODEC_SAMPLE_LOGI("VD buffer is too late"); + } else { + AVCODEC_SAMPLE_LOGE("VD buffer is too early waitTimeUs:%{public}ld", waitTimeUs); + // [0, ), render it wait waitTimeUs, max 1s + // [-40, 0), render it + if (waitTimeUs > WAIT_TIME_US_THRESHOLD) { + waitTimeUs = WAIT_TIME_US_THRESHOLD; + } + // per frame render time reduced by frame interval + if (waitTimeUs > sampleInfo_.frameInterval + perSinkTimeThreshold) { + waitTimeUs = sampleInfo_.frameInterval + perSinkTimeThreshold; + AVCODEC_SAMPLE_LOGE("VD buffer is too early and reduced, waitTimeUs: %{public}ld", waitTimeUs); + } + } + return true; +} + +bool Player::RenderAndRelease(CodecBufferInfo& bufferInfo, int64_t waitTimeUs, bool dropFrame) +{ + if (static_cast(waitTimeUs) > VSYNC_TIME * LIP_SYNC_BALANCE_VALUE) { + std::this_thread::sleep_for(std::chrono::microseconds( + static_cast(static_cast(waitTimeUs) - VSYNC_TIME * LIP_SYNC_BALANCE_VALUE))); + } + DumpOutput(bufferInfo); + int32_t ret = videoDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, + sampleInfo_.codecRunMode ? false : !dropFrame, + VSYNC_TIME * LIP_SYNC_BALANCE_VALUE * MS_TO_S + GetCurrentTime()); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGE("FreeOutputBuffer failed: %{public}d", ret); + return false; + } + return true; +} + +bool Player::ProcessVideoWithAudio(CodecBufferInfo& bufferInfo, + std::chrono::time_point& lastPushTime, int64_t perSinkTimeThreshold) +{ + // get audio render position + int64_t framePosition = 0; + int64_t timestamp = 0; + int32_t ret = OH_AudioRenderer_GetTimestamp(audioRenderer_, CLOCK_MONOTONIC, &framePosition, ×tamp); + AVCODEC_SAMPLE_LOGI("VD framePosition: %{public}li, nowTimeStamp: %{public}li", framePosition, nowTimeStamp); + audioTimeStamp = timestamp; // ns + + // audio render getTimeStamp error, render it + if (ret != AUDIOSTREAM_SUCCESS || (timestamp == 0) || (framePosition == 0)) { + DumpOutput(bufferInfo); + // first frame, render without wait + ret = videoDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, sampleInfo_.codecRunMode ? false : true, + GetCurrentTime()); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGW("FreeOutputBuffer failed: %{public}d", ret); + return false; + } + std::this_thread::sleep_until(lastPushTime + std::chrono::microseconds(sampleInfo_.frameInterval)); + lastPushTime = std::chrono::system_clock::now(); + return true; + } + int64_t waitTimeUs = 0; + bool dropFrame = false; + if (!CalculateSyncParameters(bufferInfo, framePosition, waitTimeUs, + dropFrame, perSinkTimeThreshold)) { + return false; + } + return RenderAndRelease(bufferInfo, waitTimeUs, dropFrame); +} + +void Player::VideoDecOutputThread() +{ + sampleInfo_.frameInterval = MICROSECOND_TO_S / sampleInfo_.frameRate; + int64_t perSinkTimeThreshold = MS_TO_S / sampleInfo_.frameRate * MS_TO_S; // max per sink time + while (true) { + thread_local auto lastPushTime = std::chrono::system_clock::now(); + CHECK_AND_BREAK_LOG(isStarted_, "VD Decoder output thread out"); + std::unique_lock lock(videoDecContext_->outputMutex); + bool condRet = videoDecContext_->outputCond.wait_for( + lock, 5s, [this]() { return !isStarted_ || !videoDecContext_->outputBufferInfoQueue.empty(); }); + CHECK_AND_BREAK_LOG(isStarted_, "VD Decoder output thread out"); + CHECK_AND_CONTINUE_LOG(!videoDecContext_->outputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + CodecBufferInfo bufferInfo = videoDecContext_->outputBufferInfoQueue.front(); + videoDecContext_->outputBufferInfoQueue.pop(); + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out"); + 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(); + bool success = false; + if (!audioDecContext_) { + success = ProcessVideoWithoutAudio(bufferInfo, lastPushTime); + } else { + success = ProcessVideoWithAudio(bufferInfo, lastPushTime, + perSinkTimeThreshold); + } + if (!success) { + break; + } + } + writtenSampleCnt = 0; + audioBufferPts = 0; + std::unique_lock lock(doneMutex); + isVideoDone.store(true); + lock.unlock(); + doneCond_.notify_all(); + StartRelease(); +} + +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"); + } +} + +bool Player::ProcessAudioOutput(CodecBufferInfo &bufferInfo) +{ + int32_t ret = audioDecoder_->FreeOutputBuffer(bufferInfo.bufferIndex, true); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + AVCODEC_SAMPLE_LOGW("FreeOutputBuffer failed: %{public}d", ret); + return false; + } + + // SAMPLE_S16LE 2 bytes per frame + writtenSampleCnt += (bufferInfo.attr.size / sampleInfo_.audioChannelCount / BYTES_PER_SAMPLE_2); + AVCODEC_SAMPLE_LOGI("writtenSampleCnt_: %{public}ld, bufferInfo.attr.size: %{public}d, " + "sampleInfo_.audioChannelCount: %{public}d", + writtenSampleCnt, bufferInfo.attr.size, sampleInfo_.audioChannelCount); + + audioBufferPts = bufferInfo.attr.pts; + audioDecContext_->endPosAudioBufferPts = audioBufferPts; + + std::unique_lock lockRender(audioDecContext_->renderMutex); + audioDecContext_->renderCond.wait_for(lockRender, 20ms, [this, bufferInfo]() { + return audioDecContext_->renderQueue.size() < BALANCE_VALUE * bufferInfo.attr.size; + }); + + return true; +} + +void Player::AudioDecOutputThread() +{ + 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)); + // 将解码后的PCM数据放入队列中 + for (int i = 0; i < bufferInfo.attr.size; i++) { + audioDecContext_->renderQueue.push(*(source + i)); + } + +#ifdef DEBUG_DECODE + if (audioOutputFile_.is_open()) { + audioOutputFile_.write( + (const char *)OH_AVBuffer_GetAddr(reinterpret_cast(bufferInfo.buffer)), + bufferInfo.attr.size); + } +#endif + lock.unlock(); + if (!ProcessAudioOutput(bufferInfo)) { + break; + } + } + std::unique_lock lockRender(audioDecContext_->renderMutex); + audioDecContext_->renderCond.wait_for(lockRender, 500ms, + [this]() { return audioDecContext_->renderQueue.size() < 1; }); + AVCODEC_SAMPLE_LOGI("Out buffer end"); + std::unique_lock lock(doneMutex); + isAudioDone = true; + lock.unlock(); + doneCond_.notify_all(); + StartRelease(); +} + +int64_t Player::GetCurrentTime() +{ + auto now = std::chrono::steady_clock::now(); + auto now_ns = std::chrono::time_point_cast(now); + return now_ns.time_since_epoch().count(); +} diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/Player.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/Player.h new file mode 100644 index 0000000000000000000000000000000000000000..0297f031b78c3dfe2792e6d3d40332a538f90243 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/Player.h @@ -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. + */ + +#ifndef VIDEO_CODEC_PLAYER_H +#define VIDEO_CODEC_PLAYER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "video_decoder.h" +#include "audio_decoder.h" +#include "multimedia/player_framework/native_avbuffer.h" +#include "demuxer.h" +#include "sample_info.h" +#include "plugin_manager.h" + +class Player { +public: + Player(){}; + ~Player(); + + static Player& GetInstance() + { + static Player player; + return player; + } + + int32_t Init(SampleInfo &sampleInfo); + int32_t Start(); + void SetSpeed(float multiplier); + +private: + void VideoDecInputThread(); + void VideoDecOutputThread(); + void AudioDecInputThread(); + void AudioDecOutputThread(); + void Release(); + void StartRelease(); + void ReleaseThread(); + int32_t CreateAudioDecoder(); + int32_t CreateVideoDecoder(); + int64_t GetCurrentTime(); + void DumpOutput(CodecBufferInfo &bufferInfo); + void WriteOutputFileWithStrideYUV420P(uint8_t *bufferAddr); + void WriteOutputFileWithStrideYUV420SP(uint8_t *bufferAddr); + void WriteOutputFileWithStrideRGBA(uint8_t *bufferAddr); + int32_t HandleInitError(std::unique_lock& outerLock); + int32_t StartVideoDecoder(); + int32_t StartAudioDecoder(); + void CleanupAfterStartFailure(); + bool ProcessAudioOutput(CodecBufferInfo &bufferInfo); + bool ProcessVideoWithoutAudio(CodecBufferInfo& bufferInfo, + std::chrono::time_point& lastPushTime); + bool ProcessVideoWithAudio(CodecBufferInfo& bufferInfo, + std::chrono::time_point& lastPushTime, int64_t perSinkTimeThreshold); + bool CalculateSyncParameters(CodecBufferInfo& bufferInfo, int64_t framePosition, + int64_t& waitTimeUs, bool& dropFrame, + int64_t perSinkTimeThreshold); + bool RenderAndRelease(CodecBufferInfo& bufferInfo, int64_t waitTimeUs, bool dropFrame); + + std::unique_ptr outputFile_ = nullptr; + std::unique_ptr videoDecoder_ = nullptr; + std::shared_ptr audioDecoder_ = nullptr; + std::unique_ptr demuxer_ = nullptr; + + std::mutex mutex_; + std::atomic isStarted_ { false }; + std::atomic isReleased_ { false }; + std::atomic isAudioDone { false }; + std::atomic isVideoDone { false }; + std::unique_ptr videoDecInputThread_ = nullptr; + std::unique_ptr videoDecOutputThread_ = nullptr; + std::unique_ptr audioDecInputThread_ = nullptr; + std::unique_ptr audioDecOutputThread_ = nullptr; + std::condition_variable doneCond_; + std::mutex doneMutex; + SampleInfo sampleInfo_; + CodecUserData *videoDecContext_ = nullptr; + CodecUserData *audioDecContext_ = nullptr; + OH_AudioStreamBuilder* builder_ = nullptr; + OH_AudioRenderer* audioRenderer_ = nullptr; + + int64_t nowTimeStamp = 0; + int64_t audioTimeStamp = 0; + int64_t writtenSampleCnt = 0; + int64_t audioBufferPts = 0; +#ifdef DEBUG_DECODE + std::ofstream audioOutputFile_; // for debug +#endif + float speed = 1.0f; +}; + +#endif // VIDEO_CODEC_PLAYER_H \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/PlayerNative.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/PlayerNative.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2c27d1387ea70cfb417d85cdc019b1fff33b2c97 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/PlayerNative.cpp @@ -0,0 +1,119 @@ +/* + * 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" +#include "dfx/error/av_codec_sample_error.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; + // 管理 napi_value 的生命周期,防止内存泄露 + napi_open_handle_scope(context->env, &scope); + napi_value callback = nullptr; + napi_get_reference_value(context->env, context->callbackRef, &callback); + // 回调至UI侧 + napi_call_function(context->env, nullptr, callback, 0, nullptr, nullptr); + napi_close_handle_scope(context->env, scope); + delete context; + delete work; + }); +} + +napi_value PlayerNative::SetPlaybackSpeed(napi_env env, napi_callback_info info) +{ + double speed; + size_t argc = 1; + napi_value args[1] = {nullptr}; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + napi_get_value_double(env, args[0], &speed); + Player::GetInstance().SetSpeed(static_cast(speed)); + return nullptr; +} + +napi_value PlayerNative::Play(napi_env env, napi_callback_info info) +{ + SampleInfo sampleInfo; + size_t argc = 6; // 参数个数,这里ArkTS往native测传递了四个参数,故此处赋值为4 + napi_value args[6] = {nullptr}; // napi_value类型数组,用于存储接收的ArkTS侧参数 + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); // 从info中获取参数信息到参数数组args[] + + int index = 0; + napi_get_value_int32(env, args[index++], &sampleInfo.inputFd); + napi_get_value_int64(env, args[index++], &sampleInfo.inputFileOffset); + napi_get_value_int64(env, args[index++], &sampleInfo.inputFileSize); + napi_get_value_int32(env, args[index++], &sampleInfo.codecType); + napi_get_value_int32(env, args[index++], &sampleInfo.codecRunMode); + + auto asyncContext = new CallbackContext(); + asyncContext->env = env; + napi_create_reference(env, args[index], 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; +} + +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}, + {"setPlaybackSpeed", nullptr, PlayerNative::SetPlaybackSpeed, nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + + NativeXComponentSample::PluginManager::GetInstance()->Export(env, exports); + 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/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/PlayerNative.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/PlayerNative.h new file mode 100644 index 0000000000000000000000000000000000000000..828a4ce28ad908394cf45d86e57f82ab6e565e6a --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/player/PlayerNative.h @@ -0,0 +1,34 @@ +/* + * 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 "Player.h" +#include "av_codec_sample_log.h" +#include "dfx/error/av_codec_sample_error.h" +#include "napi/native_api.h" +#include "plugin_manager.h" +#include +#include +#include +#include + +class PlayerNative { +public: + static napi_value Play(napi_env env, napi_callback_info info); + static napi_value SetPlaybackSpeed(napi_env env, napi_callback_info info); +}; +#endif // VIDEO_CODEC_SAMPLE_PLAYER_NATIVE_H \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/Recorder.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/Recorder.cpp new file mode 100644 index 0000000000000000000000000000000000000000..82d3cbfeb685d60dabb9ba5c4d7b2a22343baef9 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/Recorder.cpp @@ -0,0 +1,187 @@ +/* + * 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 "Recorder.h" +#include +#include "av_codec_sample_log.h" +#include "dfx/error/av_codec_sample_error.h" + +#undef LOG_TAG +#define LOG_TAG "recorder" + +namespace { +using namespace std::chrono_literals; +constexpr int64_t MICROSECOND = 1000000; +} + +Recorder::~Recorder() { StartRelease(); } + +int32_t Recorder::Init(SampleInfo &sampleInfo) +{ + std::lock_guard lock(mutex_); + CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + CHECK_AND_RETURN_RET_LOG(videoEncoder_ == nullptr && muxer_ == nullptr, + AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + + sampleInfo_ = sampleInfo; + + videoEncoder_ = std::make_unique(); + muxer_ = std::make_unique(); + + int32_t ret = videoEncoder_->Create(sampleInfo_.videoCodecMime); + 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); + + encContext_ = new CodecUserData; + encContext_->isEncFirstFrame = true; + ret = videoEncoder_->Config(sampleInfo_, encContext_); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder config failed"); + + ret = muxer_->Config(sampleInfo_); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Recorder muxer config failed"); + + sampleInfo.window = sampleInfo_.window; + + releaseThread_ = nullptr; + AVCODEC_SAMPLE_LOGI("Succeed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Recorder::Start() +{ + std::lock_guard lock(mutex_); + CHECK_AND_RETURN_RET_LOG(!isStarted_, AVCODEC_SAMPLE_ERR_ERROR, "Already started."); + CHECK_AND_RETURN_RET_LOG(encContext_ != nullptr, AVCODEC_SAMPLE_ERR_ERROR, + "Already started."); + 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"); + + isStarted_ = true; + encOutputThread_ = std::make_unique(&Recorder::EncOutputThread, this); + if (encOutputThread_ == nullptr) { + AVCODEC_SAMPLE_LOGE("Create thread failed"); + StartRelease(); + return AVCODEC_SAMPLE_ERR_ERROR; + } + + AVCODEC_SAMPLE_LOGI("Succeed"); + return AVCODEC_SAMPLE_ERR_OK; +} + +void Recorder::EncOutputThread() +{ + while (true) { + CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out"); + std::unique_lock lock(encContext_->outputMutex); + bool condRet = encContext_->outputCond.wait_for( + lock, 5s, [this]() { return !isStarted_ || !encContext_->outputBufferInfoQueue.empty(); }); + CHECK_AND_BREAK_LOG(isStarted_, "Work done, thread out"); + CHECK_AND_CONTINUE_LOG(!encContext_->outputBufferInfoQueue.empty(), + "Buffer queue is empty, continue, cond ret: %{public}d", condRet); + + CodecBufferInfo bufferInfo = encContext_->outputBufferInfoQueue.front(); + encContext_->outputBufferInfoQueue.pop(); + CHECK_AND_BREAK_LOG(!(bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_EOS), "Catch EOS, thread out"); + lock.unlock(); + if ((bufferInfo.attr.flags & AVCODEC_BUFFER_FLAGS_SYNC_FRAME) || + (bufferInfo.attr.flags == AVCODEC_BUFFER_FLAGS_NONE)) { + encContext_->outputFrameCount++; + bufferInfo.attr.pts = encContext_->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, + encContext_->outputFrameCount, bufferInfo.attr.size, bufferInfo.attr.flags, + bufferInfo.attr.pts); + + muxer_->WriteSample(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", encContext_->outputFrameCount); + StartRelease(); +} + +void Recorder::StartRelease() +{ + if (releaseThread_ == nullptr) { + AVCODEC_SAMPLE_LOGI("Start release CodecTest"); + releaseThread_ = std::make_unique(&Recorder::Release, this); + } +} + +void Recorder::Release() +{ + std::lock_guard lock(mutex_); + isStarted_ = false; + if (encOutputThread_ && encOutputThread_->joinable()) { + encOutputThread_->join(); + encOutputThread_.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 (encContext_ != nullptr) { + delete encContext_; + encContext_ = nullptr; + } + doneCond_.notify_all(); + AVCODEC_SAMPLE_LOGI("Succeed"); +} + +int32_t Recorder::WaitForDone() +{ + AVCODEC_SAMPLE_LOGI("Wait called"); + std::unique_lock lock(mutex_); + doneCond_.wait(lock); + if (releaseThread_ && releaseThread_->joinable()) { + releaseThread_->join(); + releaseThread_.reset(); + } + AVCODEC_SAMPLE_LOGI("Done"); + return AVCODEC_SAMPLE_ERR_OK; +} + +int32_t Recorder::Stop() +{ + if (isStarted_) { + int32_t ret = videoEncoder_->NotifyEndOfStream(); + CHECK_AND_RETURN_RET_LOG(ret == AVCODEC_SAMPLE_ERR_OK, ret, "Encoder notifyEndOfStream failed"); + } else { + StartRelease(); + } + return WaitForDone(); +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/Recorder.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/Recorder.h new file mode 100644 index 0000000000000000000000000000000000000000..5a744dab4736b9ede4f9396aa4b91c832e50ebfb --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/Recorder.h @@ -0,0 +1,62 @@ +/* + * 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_RECODER_H +#define VIDEO_CODEC_SAMPLE_RECODER_H + +#include +#include +#include +#include +#include +#include +#include "video_encoder.h" +#include "muxer.h" +#include "sample_info.h" + +class Recorder { +public: + Recorder(){}; + ~Recorder(); + + static Recorder &GetInstance() + { + static Recorder recorder; + return recorder; + } + + int32_t Init(SampleInfo &sampleInfo); + int32_t Start(); + int32_t Stop(); + +private: + void EncOutputThread(); + void Release(); + void StartRelease(); + int32_t WaitForDone(); + + std::unique_ptr videoEncoder_ = nullptr; + std::unique_ptr muxer_ = nullptr; + + std::mutex mutex_; + std::atomic isStarted_{false}; + std::unique_ptr encOutputThread_ = nullptr; + std::unique_ptr releaseThread_ = nullptr; + std::condition_variable doneCond_; + SampleInfo sampleInfo_; + CodecUserData *encContext_ = nullptr; +}; + +#endif // VIDEO_CODEC_SAMPLE_RECODER_H diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/RecorderNative.cpp b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/RecorderNative.cpp new file mode 100644 index 0000000000000000000000000000000000000000..32e541b691cb66601ee649647afdf508d41d22a5 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/RecorderNative.cpp @@ -0,0 +1,212 @@ +/* + * 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 "RecorderNative.h" +#include + +#undef LOG_DOMAIN +#undef LOG_TAG +#define LOG_DOMAIN 0xFF00 +#define LOG_TAG "recorder" + +namespace { +constexpr int RGBA = 3; +} + +struct AsyncCallbackInfo { + napi_env env; + napi_async_work asyncWork; + napi_deferred deferred; + int32_t resultCode = 0; + std::string surfaceId = ""; + SampleInfo sampleInfo; +}; + +void DealCallBack(napi_env env, void *data) +{ + AsyncCallbackInfo *asyncCallbackInfo = static_cast(data); + napi_value code; + napi_create_int32(env, asyncCallbackInfo->resultCode, &code); + napi_value surfaceId; + napi_create_string_utf8(env, asyncCallbackInfo->surfaceId.data(), NAPI_AUTO_LENGTH, &surfaceId); + napi_value obj; + napi_create_object(env, &obj); + + napi_set_named_property(env, obj, "code", code); + napi_set_named_property(env, obj, "surfaceId", surfaceId); + napi_resolve_deferred(asyncCallbackInfo->env, asyncCallbackInfo->deferred, obj); + napi_delete_async_work(env, asyncCallbackInfo->asyncWork); + delete asyncCallbackInfo; +} + +void SetCallBackResult(AsyncCallbackInfo *asyncCallbackInfo, int32_t code) +{ + asyncCallbackInfo->resultCode = code; +} + +void SurfaceIdCallBack(AsyncCallbackInfo *asyncCallbackInfo, std::string surfaceId) +{ + asyncCallbackInfo->surfaceId = surfaceId; +} + +void NativeInit(void *data) +{ + AsyncCallbackInfo *asyncCallbackInfo = static_cast(data); + int32_t ret = Recorder::GetInstance().Init(asyncCallbackInfo->sampleInfo); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + SetCallBackResult(asyncCallbackInfo, -1); + } + + uint64_t id = 0; + ret = OH_NativeWindow_GetSurfaceId(asyncCallbackInfo->sampleInfo.window, &id); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + SetCallBackResult(asyncCallbackInfo, -1); + } + asyncCallbackInfo->surfaceId = std::to_string(id); + SurfaceIdCallBack(asyncCallbackInfo, asyncCallbackInfo->surfaceId); +} + +static SampleInfo ParseSampleInfo(napi_env env, napi_value args[]) +{ + SampleInfo sampleInfo; + int index = 0; + napi_get_value_int32(env, args[index++], &sampleInfo.outputFd); + char videoCodecMime[20] = {0}; + size_t videoCodecMimeStrlen; + size_t len = 20; + napi_get_value_string_utf8(env, args[index++], videoCodecMime, len, &videoCodecMimeStrlen); + napi_get_value_int32(env, args[index++], &sampleInfo.videoWidth); + napi_get_value_int32(env, args[index++], &sampleInfo.videoHeight); + napi_get_value_double(env, args[index++], &sampleInfo.frameRate); + napi_get_value_int32(env, args[index++], &sampleInfo.isHDRVivid); + napi_get_value_int64(env, args[index++], &sampleInfo.bitrate); + + int32_t format; + if (napi_ok == napi_get_value_int32(env, args[index], &format)) { + sampleInfo.pixelFormat = (format == RGBA) ? AV_PIXEL_FORMAT_RGBA : AV_PIXEL_FORMAT_NV12; + } + + sampleInfo.videoCodecMime = videoCodecMime; + if (sampleInfo.isHDRVivid) { + sampleInfo.hevcProfile = HEVC_PROFILE_MAIN_10; + } + return sampleInfo; +} + +static AsyncCallbackInfo* CreateAsyncInfo(napi_env env, napi_deferred deferred, SampleInfo sampleInfo) +{ + AsyncCallbackInfo* asyncInfo = new AsyncCallbackInfo(); + asyncInfo->env = env; + asyncInfo->deferred = deferred; + asyncInfo->sampleInfo = sampleInfo; + asyncInfo->resultCode = -1; + return asyncInfo; +} + +static void StartAsyncWork(napi_env env, AsyncCallbackInfo* asyncInfo) +{ + napi_value resourceName; + napi_create_string_latin1(env, "recorder", NAPI_AUTO_LENGTH, &resourceName); + napi_create_async_work(env, nullptr, resourceName, + [](napi_env env, void* data) { NativeInit(data); }, + [](napi_env env, napi_status status, void* data) { DealCallBack(env, data); }, + asyncInfo, &asyncInfo->asyncWork); + napi_queue_async_work(env, asyncInfo->asyncWork); +} + +napi_value RecorderNative::Init(napi_env env, napi_callback_info info) +{ + size_t argc = 8; + napi_value args[8] = {nullptr}; + napi_get_cb_info(env, info, &argc, args, nullptr, nullptr); + + SampleInfo sampleInfo = ParseSampleInfo(env, args); + + napi_value promise; + napi_deferred deferred; + napi_create_promise(env, &deferred, &promise); + + AsyncCallbackInfo* asyncInfo = CreateAsyncInfo(env, deferred, sampleInfo); + StartAsyncWork(env, asyncInfo); + return promise; +} + +napi_value RecorderNative::Start(napi_env env, napi_callback_info info) +{ + Recorder::GetInstance().Start(); + return nullptr; +} + +void NativeStop(napi_env env, void *data) +{ + AsyncCallbackInfo *asyncCallbackInfo = static_cast(data); + int32_t ret = Recorder::GetInstance().Stop(); + if (ret != AVCODEC_SAMPLE_ERR_OK) { + SetCallBackResult(asyncCallbackInfo, -1); + } + SetCallBackResult(asyncCallbackInfo, 0); +} + +napi_value RecorderNative::Stop(napi_env env, napi_callback_info info) +{ + napi_value promise; + napi_deferred deferred; + napi_create_promise(env, &deferred, &promise); + + AsyncCallbackInfo *asyncCallbackInfo = new AsyncCallbackInfo(); + + asyncCallbackInfo->env = env; + asyncCallbackInfo->asyncWork = nullptr; + asyncCallbackInfo->deferred = deferred; + + napi_value resourceName; + napi_create_string_latin1(env, "recorder", NAPI_AUTO_LENGTH, &resourceName); + napi_create_async_work( + env, nullptr, resourceName, [](napi_env env, void *data) { NativeStop(env, data); }, + [](napi_env env, napi_status status, void *data) { DealCallBack(env, data); }, (void *)asyncCallbackInfo, + &asyncCallbackInfo->asyncWork); + napi_queue_async_work(env, asyncCallbackInfo->asyncWork); + return promise; +} + +EXTERN_C_START +static napi_value Init(napi_env env, napi_value exports) +{ + napi_property_descriptor classProp[] = { + {"initNative", nullptr, RecorderNative::Init, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"startNative", nullptr, RecorderNative::Start, nullptr, nullptr, nullptr, napi_default, nullptr}, + {"stopNative", nullptr, RecorderNative::Stop, nullptr, nullptr, nullptr, napi_default, nullptr}, + }; + + napi_value RecorderNative = nullptr; + const char *classBindName = "recorderNative"; + napi_define_class(env, classBindName, strlen(classBindName), nullptr, nullptr, 1, classProp, &RecorderNative); + napi_define_properties(env, exports, sizeof(classProp) / sizeof(classProp[0]), classProp); + return exports; +} +EXTERN_C_END + +static napi_module RecorderModule = { + .nm_version = 1, + .nm_flags = 0, + .nm_filename = nullptr, + .nm_register_func = Init, + .nm_modname = "recorder", + .nm_priv = ((void *)0), + .reserved = {0}, +}; + + +extern "C" __attribute__((constructor)) void RegisterRecorderModule(void) { napi_module_register(&RecorderModule); } diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/RecorderNative.h b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/RecorderNative.h new file mode 100644 index 0000000000000000000000000000000000000000..e5d996c02f10ee72f81719ca43ed1cbcbaee501b --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/sample/recorder/RecorderNative.h @@ -0,0 +1,35 @@ +/* + * 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_RECODER_NATIVE_H +#define VIDEO_CODEC_SAMPLE_RECODER_NATIVE_H + +#include +#include +#include +#include +#include "napi/native_api.h" +#include "Recorder.h" +#include "dfx/error/av_codec_sample_error.h" +#include "av_codec_sample_log.h" + +class RecorderNative { +public: + static napi_value Init(napi_env env, napi_callback_info info); + static napi_value Start(napi_env env, napi_callback_info info); + static napi_value Stop(napi_env env, napi_callback_info info); +}; + +#endif // VIDEO_CODEC_SAMPLE_RECODER_NATIVE_H diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/libplayer/index.d.ts b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/libplayer/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..7ac8f09745180e4b45b8678d8757f9863fe485c2 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/libplayer/index.d.ts @@ -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. + */ + +export const playNative: ( + inputFileFd: number, + inputFileOffset: number, + inputFileSize: number, + videoDecoderType: number, + videoDecoderRunMode: number, + cbFn: () => void +) => void + +export const setPlaybackSpeed: ( + speed: number, +) => void \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/libplayer/oh-package.json5 b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/libplayer/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..98c205eb7196dd0d64f8c5749523f3f411b6c372 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/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/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/librecorder/index.d.ts b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/librecorder/index.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b83151f471b689853eda07d61a7c8da8125bf31 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/librecorder/index.d.ts @@ -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. + */ + +export const initNative: (fd: number, videoCodecMime: string, width: number, height: number, + frameRate: number, isHDRVivid: number, bitRate: number, format: number) => Promise + +export const startNative: () => void + +export const stopNative: () => Promise + +export class Response { + code: number + surfaceId: string +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/librecorder/oh-package.json5 b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/librecorder/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..df5e61c0d0cfbf18475369626d5c891099b5f384 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/cpp/types/librecorder/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": "librecorder.so", + "types": "./index.d.ts", + "version": "1.0.0", + "decription": "Recorder interface." +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/CommonConstants.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/CommonConstants.ets new file mode 100644 index 0000000000000000000000000000000000000000..fee51dc9d678863868e36002900626b4228f5586 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/CommonConstants.ets @@ -0,0 +1,189 @@ +/* + * 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 { + /** + * Full size. + */ + public static readonly FULL_SIZE: string = '100%'; + /** + * Default width. + */ + public static readonly DEFAULT_WIDTH: number = 1920; + /** + * Default height. + */ + public static readonly DEFAULT_HEIGHT: number = 1080; + /** + * Button width. + */ + public static readonly BUTTON_WIDTH: number = 100; + /** + * Duration. + */ + public static readonly DURATION: number = 2000; + /** + * The distance between toast dialog box and the bottom of screen. + */ + public static readonly BOTTOM: number = 200; + /** + * Image width. + */ + public static readonly IMAGE_WIDTH: number = 50; + /** + * Image height. + */ + public static readonly IMAGE_HEIGHT: number = 50; + /** + * Video mime type. + */ + public static readonly VIDEO_MIMETYPE: string[] = ['HDRVivid', 'H264', 'H265']; + /** + * Video resolution. + */ + public static readonly VIDEO_RESOLUTION: string[] = ['4K', '1080P', '720P']; + /** + * Video framerate. + */ + public static readonly VIDEO_FRAMERATE: string[] = ['30Fps', '60Fps']; + /** + * Video decode type (hardware vs software). + */ + public static readonly VIDEO_DECODE_TYPE: string[] = ['Auto', 'Hardware', 'Software']; + /** + * Video decoder run modes. + */ + public static readonly VIDEO_DECODER_RUN_MODE: string[] = ['SurfaceMode', 'BufferMode']; + /** + * Video playerInfo. + */ + public static readonly PLAYER_INFO: string[][] = [ + CommonConstants.VIDEO_DECODE_TYPE, CommonConstants.VIDEO_DECODER_RUN_MODE + ]; + /** + * Video recorderInfo. + */ + public static readonly RECORDER_INFO: string[][] = [ + CommonConstants.VIDEO_MIMETYPE, CommonConstants.VIDEO_RESOLUTION, CommonConstants.VIDEO_FRAMERATE + ]; + /** + * Playback speed. + */ + public static readonly PLAYBACK_SPEED: string[] = ['X1', 'X2', 'X3']; + /** + * Default value. + */ + public static readonly DEFAULT_VALUE: number = 0; + /** + * Video avc mime type. + */ + public static readonly MIME_VIDEO_AVC: string = 'video/avc'; + /** + * Video hevc mime type. + */ + public static readonly MIME_VIDEO_HEVC: string = 'video/hevc'; + /** + * 2M bitrate. + */ + public static readonly BITRATE_VIDEO_2M: number = 2 * 1024 * 1024; + /** + * 10M bitrate. + */ + public static readonly BITRATE_VIDEO_10M: number = 10 * 1024 * 1024; + /** + * 20M bitrate. + */ + public static readonly BITRATE_VIDEO_20M: number = 20 * 1024 * 1024; + /** + * 30M bitrate. + */ + public static readonly BITRATE_VIDEO_30M: number = 30 * 1024 * 1024; + /** + * 30 FPS. + */ + public static readonly FRAMERATE_VIDEO_30FPS: number = 30; + /** + * 60 FPS. + */ + public static readonly FRAMERATE_VIDEO_60FPS: number = 60; + /** + * Row space. + */ + public static readonly ROW_SPACE: number = 10; + /** + * Default picker item height. + */ + public static readonly DEFAULT_PICKER_ITEM_HEIGHT: number = 30; + /** + * Selected text style font size. + */ + public static readonly SELECTED_TEXT_STYLE_FONT_SIZE: number = 15; + /** + * The number corresponding to true. + */ + public static readonly TRUE: number = 1; + /** + * The number corresponding to false. + */ + public static readonly FALSE: number = 0; + /** + * 4K video width. + */ + public static readonly VIDEO_WIDTH_4K: number = 3840; + /** + * 1080P video width. + */ + public static readonly VIDEO_WIDTH_1080P: number = 1920; + /** + * 720P video width. + */ + public static readonly VIDEO_WIDTH_720P: number = 1280; + /** + * 4K video height. + */ + public static readonly VIDEO_HEIGHT_4K: number = 2160; + /** + * 1080P video height. + */ + public static readonly VIDEO_HEIGHT_1080P: number = 1080; + /** + * 720P video height. + */ + public static readonly VIDEO_HEIGHT_720P: number = 720; + /** + * Album, photo, video, and camera switch icons size. + */ + public static readonly ICON_SIZE: number = 200; + /** + * Bottom position. + */ + public static readonly BOTTOM_POSITION: number = 0.8; + /** + * PX. + */ + public static readonly PX: string = 'px'; + /** + * Default profile. + */ + public static readonly DEFAULT_PROFILE: camera.Profile = { + format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP, + size: { + width: 1920, + height: 1080 + } + }; +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/CameraCheck.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/CameraCheck.ets new file mode 100644 index 0000000000000000000000000000000000000000..ddd9dfe4cbd94a1bfb9c6a236886ffc0321c5266 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/CameraCheck.ets @@ -0,0 +1,159 @@ +/* + * 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 Logger from './Logger'; +import { CameraDataModel } from '../../model/CameraDateModel'; +import { CommonConstants as Const } from '../CommonConstants'; + +const TAG = 'CAMERA_CHECK'; + +function getPreviewProfile(previewProfiles: camera.Profile[], size: camera.Size, + isHDRVivid: number): undefined | camera.Profile { + let previewProfile: undefined | camera.Profile = previewProfiles.find((profile: camera.Profile) => { + const resolutionMatch = profile.size.width === size.width && profile.size.height === size.height; + + if (!resolutionMatch) { + return; + } + if (isHDRVivid) { + return profile.format === camera.CameraFormat.CAMERA_FORMAT_YCRCB_P010 && resolutionMatch; + } else { + return profile.format === camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP && resolutionMatch; + } + }); + return previewProfile; +} + +function setFirstPreviewProfile(previewProfiles: camera.Profile[]) { + return previewProfiles[0]; +} + +export function previewProfileCameraCheck(cameraManager: camera.CameraManager, + cameraData: CameraDataModel): undefined | camera.Profile { + let cameraDevices = cameraManager.getSupportedCameras(); + if (cameraDevices !== undefined && cameraDevices.length <= 0) { + Logger.error(TAG, 'cameraManager.getSupportedCameras error!'); + return; + } + + let profiles: camera.CameraOutputCapability = + cameraManager.getSupportedOutputCapability(cameraDevices[0], camera.SceneMode.NORMAL_VIDEO); + if (!profiles) { + Logger.error(TAG, 'cameraManager.getSupportedOutputCapability error!'); + return; + } + + let previewProfilesArray: camera.Profile[] = profiles.previewProfiles; + if (!previewProfilesArray) { + Logger.error('createOutput previewProfilesArray == null || undefined'); + return; + } + + let videoSize: camera.Size = { + width: 1920, + height: 1080 + } + let previewProfile: undefined | camera.Profile = + getPreviewProfile(previewProfilesArray, videoSize, cameraData.isHDRVivid); + if (!previewProfile) { + Logger.error('previewProfile is not found, set to first profile'); + previewProfile = setFirstPreviewProfile(previewProfilesArray); + return previewProfile; + } + return previewProfile; +} + +export function setFirstCameraProfile(cameraManager: camera.CameraManager) { + let cameraDevices = cameraManager.getSupportedCameras(); + if (cameraDevices !== undefined && cameraDevices.length <= 0) { + Logger.error(TAG, 'cameraManager.getSupportedCameras error!'); + return; + } + + let profiles: camera.CameraOutputCapability = + cameraManager.getSupportedOutputCapability(cameraDevices[0], camera.SceneMode.NORMAL_VIDEO); + if (!profiles) { + Logger.error(TAG, 'cameraManager.getSupportedOutputCapability error!'); + return; + } + + let encoderProfiles: camera.VideoProfile[] = profiles.videoProfiles; + if (!encoderProfiles) { + Logger.error(TAG, 'Get encoderProfiles error!'); + return; + } + return encoderProfiles[0]; +} + +export function encoderProfileCameraCheck(cameraManager: camera.CameraManager, + cameraData: CameraDataModel): undefined | camera.VideoProfile { + let cameraDevices = cameraManager.getSupportedCameras(); + if (cameraDevices !== undefined && cameraDevices.length <= 0) { + Logger.error(TAG, 'cameraManager.getSupportedCameras error!'); + return; + } + + let profiles: camera.CameraOutputCapability = + cameraManager.getSupportedOutputCapability(cameraDevices[0], camera.SceneMode.NORMAL_VIDEO); + if (!profiles) { + Logger.error(TAG, 'cameraManager.getSupportedOutputCapability error!'); + return; + } + + let encoderProfiles: camera.VideoProfile[] = profiles.videoProfiles; + if (!encoderProfiles) { + Logger.error(TAG, 'Get encoderProfiles error!'); + return; + } + if (cameraData.format == camera.CameraFormat.CAMERA_FORMAT_RGBA_8888) { + return encoderProfiles[0]; + } + let encoderProfile: undefined | camera.VideoProfile = encoderProfiles.find((profile: camera.VideoProfile) => { + const resolutionMatch = + profile.size.width === cameraData.cameraWidth && + profile.size.height === cameraData.cameraHeight; + + if (!resolutionMatch) { + return; + } + if (cameraData.isHDRVivid) { + if (cameraData.frameRate === Const.FRAMERATE_VIDEO_30FPS) { + return resolutionMatch && + profile.format === camera.CameraFormat.CAMERA_FORMAT_YCBCR_P010 && + profile.frameRateRange.min === 1 && + profile.frameRateRange.max === 30; + } else { + return resolutionMatch && + profile.format === camera.CameraFormat.CAMERA_FORMAT_YCBCR_P010 && + profile.frameRateRange.min === cameraData.frameRate && + profile.frameRateRange.max === cameraData.frameRate; + } + } else { + if (cameraData.frameRate === Const.FRAMERATE_VIDEO_30FPS) { + return resolutionMatch && + profile.format === camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP && + profile.frameRateRange.min === 1 && + profile.frameRateRange.max === 30; + } else { + return resolutionMatch && + profile.format === camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP && + profile.frameRateRange.min === cameraData.frameRate && + profile.frameRateRange.max === cameraData.frameRate; + } + } + }); + return encoderProfile; +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/DateTimeUtils.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/DateTimeUtils.ets new file mode 100644 index 0000000000000000000000000000000000000000..38fd74c8d720a0ecc9021b75d28dd401a54ca849 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/DateTimeUtils.ets @@ -0,0 +1,55 @@ +/* + * 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 default class DateTimeUtil { + getTime(): string { + const DATETIME = new Date(); + return this.concatTime(DATETIME.getHours(), DATETIME.getMinutes(), DATETIME.getSeconds()); + } + + getDate(): string { + const DATETIME = new Date(); + return this.concatDate(DATETIME.getFullYear(), DATETIME.getMonth() + 1, DATETIME.getDate()); + } + + fill(value: number): string { + return (value > 9 ? '' : '0') + value; + } + + concatDate(year: number, month: number, date: number): string { + return `${year}${month}${date}`; + } + + concatTime(hour: number, minute: number, second: number): string { + return `${this.fill(hour)}${this.fill(minute)}${this.fill(second)}`; + } +} + +export function getShownTimer(ms: number): string { + let seconds: number = Math.round(ms / 1000); + let sec: number = seconds % 60; + let min: number = (seconds - sec) / 60; + let secStr = (sec >= 0 && sec < 10) ? ('0' + sec) : sec; + let minStr = (min >= 0 && min < 10) ? ('0' + min) : min; + return (minStr + ':' + secStr); +} + +export function dateTime(t: number): string { + let minute: number = Math.floor(t / 60) % 60; + let m = minute < 10 ? '0' + minute : minute; + let second: number = t % 60; + let s = second < 10 ? '0' + second : second; + return m + ':' + s; +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/Logger.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/Logger.ets new file mode 100644 index 0000000000000000000000000000000000000000..530b3a380c4b12293854c5368d52579d74dfd6a8 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/common/utils/Logger.ets @@ -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. + */ + +import hilog from '@ohos.hilog' + +class Logger { + private domain: number; + private prefix: string; + private format: string = '%{public}s, %{public}s'; + + constructor(prefix: string) { + this.prefix = prefix; + this.domain = 0xFF00; + } + + debug(...args: string[]): void { + hilog.debug(this.domain, this.prefix, this.format, args); + } + + info(...args: string[]): void { + hilog.info(this.domain, this.prefix, this.format, args); + } + + warn(...args: string[]): void { + hilog.warn(this.domain, this.prefix, this.format, args); + } + + error(...args: string[]): void { + hilog.error(this.domain, this.prefix, this.format, args); + } +} + +export default new Logger('AVRecorderSample') \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/entryability/EntryAbility.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..35d314b20c1d906573b87d5adf99b47738b25696 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,96 @@ +/* + * 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 { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit' +import { BusinessError } from '@kit.BasicServicesKit' +import { window } from '@kit.ArkUI' +import abilityAccessCtrl from '@ohos.abilityAccessCtrl' +import Logger from '../common/utils/Logger' +import { hilog } from '@kit.PerformanceAnalysisKit' + +const TAG: string = 'EntryAbility' + +function requestPre() { + let atManager = abilityAccessCtrl.createAtManager(); + try { + atManager.requestPermissionsFromUser(globalThis.context, + ['ohos.permission.CAMERA',]) + .then((data) => { + Logger.info('requestPre() data: ' + JSON.stringify(data)); + }).catch((err: BusinessError) => { + Logger.info('requestPre() data: ' + JSON.stringify(err)); + }) + } catch (err) { + Logger.error('requestPre() data: ' + JSON.stringify(err)); + } +} + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { + Logger.info('Ability onCreate'); + globalThis.context = this.context; + requestPre(); + } + + onDestroy() { + Logger.info('Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + // Main window is created, set main page for this ability + Logger.info('Ability onWindowStageCreate'); + + windowStage.getMainWindowSync().setWindowKeepScreenOn(true); + windowStage.loadContent('pages/Index', (err, data) => { + if (err.code) { + Logger.error('Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + AppStorage.setOrCreate('context', windowStage.getMainWindowSync().getUIContext()); + Logger.info('Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); + }) + let windowClass: window.Window = windowStage.getMainWindowSync(); // 获取应用主窗口 + // Set the window to full screen + let isLayoutFullScreen = true; + windowClass.setWindowLayoutFullScreen(isLayoutFullScreen) + .then(() => { + hilog.info(0x0000, TAG, 'Succeeded in setting the window layout to full-screen mode. Data: %{public}s'); + }) + .catch((err: BusinessError) => { + hilog.error(0x0000, TAG, 'Failed to set the window layout to full-screen mode. Cause: %{public}s', + JSON.stringify(err) ?? ''); + }) + // Set the font color of the status bar. + let systemBarProperties: window.SystemBarProperties = { + statusBarContentColor: '#FFFFFF' + }; + windowStage.getMainWindowSync().setWindowSystemBarProperties(systemBarProperties); + } + + onWindowStageDestroy() { + // Main window is destroyed, release UI related resources + Logger.info('Ability onWindowStageDestroy'); + } + + onForeground() { + // Ability has brought to foreground + Logger.info('Ability onForeground'); + } + + onBackground() { + // Ability has back to background + Logger.info('Ability onBackground'); + } +} diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/model/CameraDateModel.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/model/CameraDateModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..1d49906881761596aebe29f048baaa078cec1f77 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/model/CameraDateModel.ets @@ -0,0 +1,42 @@ +/* + * 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 CameraDataModel { + public surfaceId: string = ''; + public cameraWidth: number = Const.DEFAULT_WIDTH; + public cameraHeight: 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; + public format: camera.CameraFormat = camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP; + + setCodecFormat(isHDR: number, codecMime: string): void { + this.isHDRVivid = isHDR; + this.videoCodecMime = codecMime; + this.format = camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP; + } + + setResolution(width: number, height: number, bit: number): void { + this.cameraWidth = width; + this.cameraHeight = height; + this.bitRate = bit; + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/pages/Index.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..0d008d37eef37e945b235611ad091d8b8e1cb38a --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,511 @@ +/* + * 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 { fileIo } from '@kit.CoreFileKit' +import { display } from '@kit.ArkUI' +import { camera } from '@kit.CameraKit' +import { photoAccessHelper } from '@kit.MediaLibraryKit' +import player from 'libplayer.so' +import recorder from 'librecorder.so' +import Logger from '../common/utils/Logger' +import DateTimeUtil from '../common/utils/DateTimeUtils' +import { CommonConstants as Const } from '../common/CommonConstants' +import { CameraDataModel } from '../model/CameraDateModel' +import { encoderProfileCameraCheck, setFirstCameraProfile } from '../common/utils/CameraCheck' +import picker from '@ohos.file.picker' + +const TAG: string = 'Sample_Player'; +const DATETIME: DateTimeUtil = new DateTimeUtil(); + +@Entry +@Component +export struct Player { + @State buttonEnabled: boolean = true; + @State isShow: boolean = false; + private videoDecoderType: number = 0; + private videoDecoderRunMode: number = 0; + private cameraData: CameraDataModel = new CameraDataModel(); + private selectFilePath: string | null = null; + private display = display.getDefaultDisplaySync(); + private sourcePath: string[] = ['从文件管理选取', '从图库选取']; + + selectFile() { + TextPickerDialog.show({ + range: this.sourcePath, + canLoop: false, + selected: 0, + onAccept: (value: TextPickerResult) => { + switch (value.value) { + case '从文件管理选取': + this.selectDocFile(); + break; + case '从图库选取': + this.selectAlbumFile(); + break; + default: + this.selectAlbumFile(); + break; + } + } + }) + } + + selectDocFile() { + let documentSelectOptions = new picker.DocumentSelectOptions; + let documentViewPicker = new picker.DocumentViewPicker; + documentViewPicker.select(documentSelectOptions) + .then((documentSelectResult) => { + this.selectFilePath = documentSelectResult[0]; + if (this.selectFilePath == null) { + this.getUIContext().getPromptAction().showToast({ + message: $r('app.string.alert'), + duration: Const.DURATION, + bottom: Const.BOTTOM + }) + } else { + this.play(); + Logger.info(TAG, 'documentViewPicker.select to file succeed and URI is:' + this.selectFilePath); + } + }) + } + + selectAlbumFile() { + 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 { + this.play(); + Logger.info(TAG, 'documentViewPicker.select to file succeed and URI is:' + this.selectFilePath); + } + }) + } + + setPlaybackSpeed(speed: number) { + player.setPlaybackSpeed(speed); + } + + play() { + let inputFile = fileIo.openSync(this.selectFilePath, fileIo.OpenMode.READ_ONLY); + 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.buttonEnabled = false; + player.playNative(inputFile.fd, Const.DEFAULT_VALUE, inputFileState.size, this.videoDecoderType, + this.videoDecoderRunMode, () => { + Logger.info(TAG, 'player JSCallback'); + this.buttonEnabled = true; + fileIo.close(inputFile); + }) + } + + async checkIsProfileSupport(): Promise { + let cameraManager: camera.CameraManager = camera.getCameraManager(getContext(this)); + if (!cameraManager) { + Logger.error(TAG, 'camera.getCameraManager error!'); + } + + let encoderProfile: undefined | camera.VideoProfile = + await encoderProfileCameraCheck(cameraManager, this.cameraData); + if (!encoderProfile) { + Logger.error(TAG, 'encoderProfile is not found'); + this.getUIContext().getPromptAction().showToast({ + message: $r('app.string.alert1'), + duration: Const.DURATION, + bottom: Const.BOTTOM, + backgroundColor: Color.White, + backgroundBlurStyle: BlurStyle.NONE + }); + this.cameraData = new CameraDataModel(); + encoderProfile = await encoderProfileCameraCheck(cameraManager, this.cameraData); + if (!encoderProfile) { + Logger.error(TAG, '1080P video profile is not found, set first camera profile'); + let encoderProfile: camera.VideoProfile = await setFirstCameraProfile(cameraManager) as camera.VideoProfile; + this.cameraData.cameraWidth = encoderProfile.size.width; + this.cameraData.cameraHeight = encoderProfile.size.height; + this.cameraData.frameRate = encoderProfile.frameRateRange.max; + this.cameraData.bitRate = Const.BITRATE_VIDEO_2M; + this.cameraData.format = encoderProfile.format; + } + return; + } else { + this.getUIContext().getPromptAction().showToast({ + message: $r('app.string.success'), + duration: Const.DURATION, + bottom: Const.BOTTOM, + backgroundColor: Color.White, + backgroundBlurStyle: BlurStyle.NONE + }); + } + } + + @Builder + PlayerSettingButton() { + Button('设置') + .size({ + width: '18.5%', + height: $r('app.float.index_button_height') + }) + .enabled(this.buttonEnabled) + .margin({ bottom: $r('app.float.button_margin_bottom') }) + .onClick(() => { + this.getUIContext().showTextPickerDialog({ + defaultPickerItemHeight: Const.DEFAULT_PICKER_ITEM_HEIGHT, + selectedTextStyle: ({ + font: ({ + size: Const.SELECTED_TEXT_STYLE_FONT_SIZE + }) + }), + range: Const.PLAYER_INFO, + canLoop: false, + alignment: DialogAlignment.Center, + backgroundColor: Color.White, + backgroundBlurStyle: BlurStyle.BACKGROUND_ULTRA_THICK, + onAccept: (value: TextPickerResult) => { + switch (value.value[0]) { + case Const.VIDEO_DECODE_TYPE[0]: { + this.videoDecoderType = 0; + break; + } + case Const.VIDEO_DECODE_TYPE[1]: { + this.videoDecoderType = 1; + break; + } + case Const.VIDEO_DECODE_TYPE[2]: { + this.videoDecoderType = 2; + break; + } + default: + break; + } + + switch (value.value[1]) { + case Const.VIDEO_DECODER_RUN_MODE[0]: { + this.videoDecoderRunMode = 0; + break; + } + case Const.VIDEO_DECODER_RUN_MODE[1]: { + this.videoDecoderRunMode = 1; + break; + } + default: + break; + } + + this.getUIContext().getPromptAction().showToast({ + message: $r('app.string.success'), + duration: Const.DURATION, + bottom: Const.BOTTOM, + backgroundColor: Color.White, + backgroundBlurStyle: BlurStyle.NONE + }); + } + }) + }) + } + + @Builder + RecorderSettingButton() { + Button('设置') + .enabled(this.buttonEnabled) + .size({ + width: '18.5%', + height: $r('app.float.index_button_height') + }) + .enabled(this.buttonEnabled) + .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.cameraData.setCodecFormat(Const.TRUE, Const.MIME_VIDEO_HEVC); + break; + } + case Const.VIDEO_MIMETYPE[1]: { + this.cameraData.setCodecFormat(Const.FALSE, Const.MIME_VIDEO_AVC); + break; + } + case Const.VIDEO_MIMETYPE[2]: { + this.cameraData.setCodecFormat(Const.FALSE, Const.MIME_VIDEO_HEVC); + break; + } + default: + break; + } + + switch (value.value[1]) { + case Const.VIDEO_RESOLUTION[0]: { + this.cameraData.setResolution(Const.VIDEO_WIDTH_4K, Const.VIDEO_HEIGHT_4K, Const.BITRATE_VIDEO_30M); + break; + } + case Const.VIDEO_RESOLUTION[1]: { + this.cameraData.setResolution(Const.VIDEO_WIDTH_1080P, Const.VIDEO_HEIGHT_1080P, + Const.BITRATE_VIDEO_20M); + break; + } + case Const.VIDEO_RESOLUTION[2]: { + this.cameraData.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.cameraData.frameRate = Const.FRAMERATE_VIDEO_30FPS; + break; + } + case Const.VIDEO_FRAMERATE[1]: { + this.cameraData.frameRate = Const.FRAMERATE_VIDEO_60FPS; + break; + } + default: + break; + } + this.checkIsProfileSupport(); + } + }) + }) + } + + @Builder + Authorized() { + Column() { + Text($r('app.string.saveButtonNote')) + .width('100%') + .fontSize('16vp') + .margin({ bottom: '12vp' }) + + Row() { + Button($r('app.string.saveButtonCancel')) + .onClick(() => { + this.isShow = false + }) + .width('48%') + + Blank() + + SaveButton({ text: SaveDescription.SAVE }) + .onClick(async () => { + const context = this.getUIContext().getHostContext(); + let helper = photoAccessHelper.getPhotoAccessHelper(context); + let uri = await helper.createAsset(photoAccessHelper.PhotoType.VIDEO, 'mp4', { + title: `AVCodecVideo_${DATETIME.getDate()}_${DATETIME.getTime()}` + }); + await this.checkIsProfileSupport(); + let file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + this.cameraData.outputfd = file.fd; + if (this.cameraData.outputfd !== null) { + recorder.initNative(this.cameraData.outputfd, this.cameraData.videoCodecMime, this.cameraData.cameraWidth, + this.cameraData.cameraHeight, this.cameraData.frameRate, this.cameraData.isHDRVivid, + this.cameraData.bitRate, this.cameraData.format).then((data) => { + if (data.surfaceId !== null) { + this.cameraData.surfaceId = data.surfaceId; + this.getUIContext().getRouter().pushUrl({ + url: 'recorder/Recorder', + params: this.cameraData + }); + } + }) + } else { + Logger.error(TAG, 'get outputfd failed!'); + } + }) + .width('48%') + .height('40vp') + } + .justifyContent(FlexAlign.SpaceAround) + .alignItems(VerticalAlign.Bottom) + .margin({ bottom: '44vp' }) + .width('100%') + .height('52vp') + } + .justifyContent(FlexAlign.End) + .padding({ left: '16vp', right: '16vp' }) + .width('100%') + .height('100%') + } + + @Builder + Window() { + Row() { + XComponent({ + id: 'player', + type: XComponentType.SURFACE, + libraryname: 'player' + }) + .height(Const.FULL_SIZE) + .width(Const.FULL_SIZE) + .gesture( + GestureGroup(GestureMode.Exclusive, + LongPressGesture({ repeat: true })// 长按动作存在会连续触发 + .onAction((event: GestureEvent | undefined) => { + if (!this.buttonEnabled) { + this.setPlaybackSpeed(2); + this.getUIContext().getPromptAction().showToast({ message: 'X2' }); + } + })// 长按动作一结束触发 + .onActionEnd(() => { + if (!this.buttonEnabled) { + this.setPlaybackSpeed(1); + this.getUIContext().getPromptAction().showToast({ message: 'X1' }); + } + }), + /** + * todo:单击播放窗口播放/暂停 + * TapGesture({ count: 1, fingers: 1 }) + * .onAction(() => { + * if (!this.isStart) { + * this.resume() + * } else { + * this.pause() + * } + * this.isStart = !this.isStart + * }) + */ + ) + ) + } + .alignRules({ + 'top': { 'anchor': '__container__', 'align': VerticalAlign.Top }, + 'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start } + }) + } + + build() { + Stack() { + this.Window() + + Column() { + Row() { + this.PlayerSettingButton() + + Blank(5) + Button(this.buttonEnabled ? $r('app.string.play') : $r('app.string.playing')) + .onClick(() => { + this.selectFile() + }) + .size({ + width: '60%', + height: $r('app.float.index_button_height') + }) + .enabled(this.buttonEnabled) + .margin({ bottom: $r('app.float.button_margin_bottom') }) + Blank(5) + Button('倍速') + .enabled(!this.buttonEnabled) + .size({ + width: '18.5%', + height: $r('app.float.index_button_height') + }) + .margin({ bottom: $r('app.float.button_margin_bottom') }) + .onClick(() => { + this.getUIContext().getPromptAction().showToast({ message: '播放时长按播放窗口也可倍速播放' }); + TextPickerDialog.show({ + defaultPickerItemHeight: Const.DEFAULT_PICKER_ITEM_HEIGHT, + selectedTextStyle: ({ + font: ({ + size: Const.SELECTED_TEXT_STYLE_FONT_SIZE + }) + }), + range: Const.PLAYBACK_SPEED, + canLoop: false, + alignment: DialogAlignment.Center, + backgroundColor: Color.White, + backgroundBlurStyle: BlurStyle.BACKGROUND_ULTRA_THICK, + onAccept: (value: TextPickerResult) => { + switch (value.value) { + case Const.PLAYBACK_SPEED[0]: { + this.setPlaybackSpeed(1); + this.getUIContext().getPromptAction().showToast({ message: 'X1' }); + break; + } + case Const.PLAYBACK_SPEED[1]: { + this.setPlaybackSpeed(2); + this.getUIContext().getPromptAction().showToast({ message: 'X2' }); + break; + } + case Const.PLAYBACK_SPEED[2]: { + this.setPlaybackSpeed(3); + this.getUIContext().getPromptAction().showToast({ message: 'X3' }); + break; + } + default: + break; + } + } + }) + }) + } + + Row() { + this.RecorderSettingButton() + Blank(5) + Button($r('app.string.record')) + .onClick(() => { + this.isShow = true; + }) + .bindSheet($$this.isShow, this.Authorized, { + height: 210, + title: { + title: $r('app.string.saveButtonTitle') + } + }) + .size({ + width: '80%', + height: $r('app.float.index_button_height') + }) + .enabled(this.buttonEnabled) + } + } + .margin({ top: (this.display.height * 0.7) + 'px' }) + .alignRules({ + 'bottom': { 'anchor': '__container__', 'align': VerticalAlign.Bottom }, + 'left': { 'anchor': '__container__', 'align': HorizontalAlign.Start } + }) + .padding({ left: '16vp', right: '16vp', bottom: '16vp' }) + .width($r('app.string.full_width')) + .height($r('app.float.index_column_height')) + .justifyContent(FlexAlign.End) + } + .width($r('app.string.full_width')) + .height($r('app.string.full_height')) + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/ets/recorder/Recorder.ets b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/recorder/Recorder.ets new file mode 100644 index 0000000000000000000000000000000000000000..544ca6ac4b0907116cc645de0f953d12f150aa9e --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/ets/recorder/Recorder.ets @@ -0,0 +1,408 @@ +/* + * 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 { fileIo } from '@kit.CoreFileKit'; +import { display } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import recorder from 'librecorder.so'; +import Logger from '../common/utils/Logger'; +import { dateTime } from '../common/utils/DateTimeUtils'; +import { CommonConstants as Const } from '../common/CommonConstants'; +import { CameraDataModel } from '../model/CameraDateModel'; +import { encoderProfileCameraCheck, previewProfileCameraCheck } from '../common/utils/CameraCheck'; +import { colorSpaceManager } from '@kit.ArkGraphics2D'; + +const TAG: string = 'Sample_Recorder'; + +const context = AppStorage.get('context') as UIContext; +const params: CameraDataModel = context.getRouter().getParams() as CameraDataModel; + +let cameraInput: camera.CameraInput; +let xComponentPreviewOutput: camera.PreviewOutput; +let encoderVideoOutput: camera.VideoOutput; +let videoSession: camera.VideoSession; + +async function releaseCamera() { + // 停止录像输出流 + encoderVideoOutput.stop((err: BusinessError) => { + if (err) { + Logger.error(TAG, `Failed to stop the encoder video output. error: ${JSON.stringify(err)}`); + return; + } + Logger.info(TAG, 'Callback invoked to indicate the encoder video output stop success.'); + }) + // 停止当前会话 + videoSession.stop(); + // 关闭文件fd + fileIo.close(params.outputfd); + // 释放相机输入流 + cameraInput.close(); + // 释放预览输出流 + xComponentPreviewOutput.release(); + // 释放录像输出流 + encoderVideoOutput.release(); + // 释放会话 + videoSession.release(); +} + +function getPreviewProfile(previewProfiles: camera.Profile[] | undefined, + size: camera.Size): undefined | camera.Profile { + if (previewProfiles) { + let previewProfile: undefined | camera.Profile = previewProfiles.find((profile: camera.Profile) => { + return profile.format === camera.CameraFormat.CAMERA_FORMAT_YCRCB_P010 && + profile.size.width === size.width && profile.size.height == size.height + }); + return previewProfile; + } + return undefined; +} + +// 查询是否支持视频防抖。HDR录像需要支持视频防抖。 +function isVideoStabilizationModeSupported(session: camera.VideoSession, mode: camera.VideoStabilizationMode): boolean { + let isSupported: boolean = false; + try { + isSupported = session.isVideoStabilizationModeSupported(mode); + } catch (error) { + // 失败返回错误码error.code并处理 + let err = error as BusinessError; + Logger.error(`The isVideoStabilizationModeSupported call failed. error code: ${err.code}`); + } + return isSupported; +} + +// 设置视频防抖。 +function setVideoStabilizationMode(session: camera.VideoSession): boolean { + let mode: camera.VideoStabilizationMode = camera.VideoStabilizationMode.AUTO; + // 查询是否支持视频防抖 + let isSupported: boolean = isVideoStabilizationModeSupported(session, mode); + if (isSupported) { + Logger.info(TAG, `setVideoStabilizationMode: ${mode}`); + // 设置视频防抖 + session.setVideoStabilizationMode(mode); + let activeVideoStabilizationMode = session.getActiveVideoStabilizationMode(); + Logger.info(TAG, `activeVideoStabilizationMode: ${activeVideoStabilizationMode}`); + } else { + Logger.info(TAG, `videoStabilizationMode: ${mode} is not support`); + } + return isSupported; +} + +// 查询支持的色彩空间。 +function getSupportedColorSpaces(session: camera.VideoSession): colorSpaceManager.ColorSpace[] { + let colorSpaces: colorSpaceManager.ColorSpace[] = []; + try { + colorSpaces = session.getSupportedColorSpaces(); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `The getSupportedColorSpaces call failed. error code: ${err.code}`); + } + return colorSpaces; +} + +// 设置色彩空间。 +function setColorSpaceBeforeCommitConfig(session: camera.VideoSession, isHdr: number): void { + let colorSpace: colorSpaceManager.ColorSpace = + isHdr ? colorSpaceManager.ColorSpace.BT2020_HLG_LIMIT : colorSpaceManager.ColorSpace.BT709_LIMIT; + let colorSpaces: colorSpaceManager.ColorSpace[] = getSupportedColorSpaces(session); + let isSupportedColorSpaces = colorSpaces.indexOf(colorSpace) >= 0; + if (isSupportedColorSpaces) { + Logger.info(TAG, `setColorSpace: ${colorSpace}`); + session.setColorSpace(colorSpace); + let activeColorSpace: colorSpaceManager.ColorSpace = session.getActiveColorSpace(); + Logger.info(TAG, `activeColorSpace: ${activeColorSpace}`); + } else { + Logger.info(TAG, `colorSpace: ${colorSpace} is not support`); + } +} + +@Entry +@Component +export struct Recorder { + private START_RECORDER: string = '开始录制'; + private STOP_RECORDER: string = '停止录制'; + @State isRecorderTimeTextHide: boolean = true; + @State buttonText: string = this.START_RECORDER; + @State buttonEnabled: boolean = true; + @State videoRecorderTimeText: string = '00:00'; + @State fov: number = 1; + private xComponentSurfaceId: string = '-1'; + private cameraWidth: number = Const.DEFAULT_WIDTH; + private cameraHeight: number = Const.DEFAULT_HEIGHT; + private xComponentController: XComponentController = new XComponentController(); + private display = display.getDefaultDisplaySync(); + private heightPx = (this.display.width * this.cameraWidth / this.cameraHeight) + 'px'; + private timer: number = Const.DEFAULT_VALUE; + private seconds: number = Const.DEFAULT_VALUE; + private isReleased: boolean = false; + private isBack: boolean = false; + private range: number[] = []; + + onBackPress() { + this.isBack = true; + } + + onPageHide() { + this.release(); + if (!this.isBack) { + this.getUIContext().getRouter().back(); + } + } + + setTimer() { + this.buttonEnabled = false; + setTimeout(async () => { + this.buttonEnabled = true; + }, 1300); + } + + async release(): Promise { + if (!this.isReleased) { + this.isReleased = true; + clearInterval(this.timer); + this.seconds = 0; + this.videoRecorderTimeText = '00:00'; + recorder.stopNative() + .then(async (data) => { + if (data.code == 0) { + await releaseCamera(); + this.buttonText == this.START_RECORDER; + } + }) + } + } + + getRecordTime(): void { + this.timer = setInterval(() => { + this.seconds += 1; + this.videoRecorderTimeText = dateTime(this.seconds); + }, 1000); + } + + async createRecorder(): Promise { + releaseCamera(); + // Create the CameraManager object. + let cameraManager = camera.getCameraManager(this.getUIContext().getHostContext()); + if (!cameraManager) { + Logger.error(TAG, 'camera.getCameraManager error'); + return; + } + + // Get supported camera devices. + let cameraDevices: camera.CameraDevice[] = cameraManager.getSupportedCameras(); + if (cameraDevices !== undefined && cameraDevices.length <= 0) { + Logger.error(TAG, 'cameraManager.getSupportedCameras error!'); + return; + } + + // 获取支持的模式类型 + let sceneModes: camera.SceneMode[] = cameraManager.getSupportedSceneModes(cameraDevices[0]); + let isSupportVideoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_VIDEO) >= 0; + if (!isSupportVideoMode) { + Logger.error('video mode not support'); + return; + } + + let encoderProfile: undefined | camera.VideoProfile = encoderProfileCameraCheck(cameraManager, params); + if (!encoderProfile) { + Logger.error(TAG, 'encoderProfile is not found!'); + return; + } + + // The preview stream of XComponent. + let xComponentPreviewProfile: camera.Profile | undefined = previewProfileCameraCheck(cameraManager, params); + if (xComponentPreviewProfile === undefined) { + Logger.error(TAG, 'XComponentPreviewProfile is not found'); + return; + } + + //Create the encoder output object + encoderVideoOutput = cameraManager.createVideoOutput(encoderProfile, params.surfaceId); + if (encoderVideoOutput === undefined) { + Logger.error(TAG, 'encoderVideoOutput is undefined'); + return; + } + Logger.info(TAG, 'encoderVideoOutput success'); + + // Create a preview stream output object + xComponentPreviewOutput = cameraManager.createPreviewOutput(xComponentPreviewProfile, this.xComponentSurfaceId); + if (xComponentPreviewOutput === undefined) { + Logger.error(TAG, 'XComponentPreviewOutput is undefined'); + return; + } + + // Create the cameraInput object. + try { + cameraInput = cameraManager.createCameraInput(cameraDevices[0]); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `Failed to createCameraInput. error: ${JSON.stringify(err)}`); + } + if (cameraInput === undefined) { + Logger.error(TAG, 'cameraInput is undefined'); + return; + } + + // Turn on the camera. + try { + await cameraInput.open(); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `Failed to open cameraInput. error: ${JSON.stringify(err)}`); + } + + // Session flow. + try { + videoSession = cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO) as camera.VideoSession; + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `Failed to create the session instance. error: ${JSON.stringify(err)}`); + } + if (videoSession === undefined) { + Logger.error(TAG, 'videoSession is undefined'); + return; + } + + // Start Configuring the session. + try { + videoSession.beginConfig(); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `Failed to beginConfig. error: ${JSON.stringify(err)}`); + } + + // Add CameraInput to the session. + try { + videoSession.addInput(cameraInput); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `Failed to add cameraInput. error: ${JSON.stringify(err)}`); + } + + // Add the XComponent preview stream to the session. + try { + videoSession.addOutput(xComponentPreviewOutput); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `Failed to add XcomponentPreviewOutput. error: ${JSON.stringify(err)}`); + } + + // Add the encoder video stream to the session. + try { + videoSession.addOutput(encoderVideoOutput); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `Failed to add encoderVideoOutput. error: ${JSON.stringify(err)}`); + } + + // Submit configuration information. + try { + await videoSession.commitConfig(); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `videoSession commitConfig error: ${JSON.stringify(err)}`); + } + + // 设置视频防抖 + if (setVideoStabilizationMode(videoSession)) { + // 设置色彩空间 + setColorSpaceBeforeCommitConfig(videoSession, params.isHDRVivid); + } + + // Session start. + try { + await videoSession.start(); + } catch (error) { + let err = error as BusinessError; + Logger.error(TAG, `videoSession start error: ${JSON.stringify(err)}`); + } + } + + build() { + Column() { + Stack() { + XComponent({ + id: 'recorderXComponent', + type: XComponentType.SURFACE, + controller: this.xComponentController + }) + .onLoad(() => { + this.xComponentSurfaceId = this.xComponentController.getXComponentSurfaceId(); + this.createRecorder(); + }) + .width(Const.FULL_SIZE) + .height(this.heightPx) + .gesture( + PinchGesture() + .onActionUpdate((event: GestureEvent) => { + if (videoSession) { + let currentFov = this.fov * event.scale; + if (currentFov > this.range[1]) { + currentFov = this.range[1]; + } + if (currentFov < this.range[0]) { + currentFov = this.range[0]; + } + videoSession.setZoomRatio(currentFov); + } + }) + .onActionEnd((event: GestureEvent) => { + if (videoSession) { + this.fov = videoSession.getZoomRatio(); + } + }) + ) + + if (!this.isRecorderTimeTextHide) { + Text(this.videoRecorderTimeText) + .fontFamily('HarmonyHeilTi-Light') + .width(100) + .height(40) + .fontSize(27) + .fontColor(Color.White) + .margin({ bottom: 500 }) + } + + Button(this.buttonText) + .onClick(() => { + if (this.buttonText == this.START_RECORDER) { + this.buttonText = this.STOP_RECORDER; + this.setTimer() + // 启动录像输出流 + encoderVideoOutput.start((err: BusinessError) => { + if (err) { + Logger.error(TAG, `Failed to start the encoder video output. error: ${JSON.stringify(err)}`) + return + } + Logger.info(TAG, 'Callback invoked to indicate the encoder video output start success.') + }); + recorder.startNative(); + this.isRecorderTimeTextHide = false; + this.getRecordTime(); + } else { + this.buttonEnabled = false; + this.isBack = true + this.release(); + this.getUIContext().getRouter().back(); + } + }) + .enabled(this.buttonEnabled) + .margin({ top: 500 }) + } + } + .justifyContent(FlexAlign.SpaceEvenly) + .height(Const.FULL_SIZE) + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/module.json5 b/code/BasicFeature/Media/AVCodec/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4f1f3815e08967d6f3edcb937f7a4191c25218ac --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/module.json5 @@ -0,0 +1,62 @@ +/* + * 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. + */ + +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "default", + "tablet" + ], + "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" + ] + } + ] + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.CAMERA", + "reason": "$string:reason", + "usedScene": { + "abilities": ["FormAbility"], + "when": "inuse" + } + } + ] + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/color.json b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..0d98b5c17780f121191f2768bdf0cd0bc6e9b9e7 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/color.json @@ -0,0 +1,28 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + }, + { + "name": "button_background", + "value": "#FFFFFF" + }, + { + "name": "homepage_background", + "value": "#F1F3F5" + }, + { + "name": "title_color", + "value": "#000000" + }, + { + "name": "button_color", + "value": "#007DFF" + }, + { + "name": "divider_color", + "value": "#182431" + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/float.json b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000000000000000000000000000000000..9dbe4cf7ee7fd4fc771a44ede454e93f6555caa2 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/float.json @@ -0,0 +1,44 @@ +{ + "float": [ + { + "name": "font_size", + "value": "20vp" + }, + { + "name": "button_height", + "value": "50vp" + }, + { + "name": "button_margin_1", + "value": "30vp" + }, + { + "name": "button_margin_2", + "value": "10vp" + }, + { + "name": "image_margin_1", + "value": "30vp" + }, + { + "name": "set_row_height", + "value": "40vp" + }, + { + "name": "set_row_margin_top", + "value": "8vp" + }, + { + "name": "button_margin_bottom", + "value": "12vp" + }, + { + "name": "index_button_height", + "value": "40vp" + }, + { + "name": "index_column_height", + "value": "200vp" + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/string.json b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..5b3a7bffb11a47281ff32f85ad19d51153a4b4ad --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/element/string.json @@ -0,0 +1,68 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "This module template implements List functions." + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "AVCodec" + }, + { + "name": "ability_desc", + "value": "This ability loads ListPage" + }, + { + "name": "reason", + "value": "label" + }, + { + "name": "playing", + "value": "playing" + }, + { + "name": "play", + "value": "play" + }, + { + "name": "alert", + "value": "None video selected" + }, + { + "name": "alert1", + "value": "This device camera does not support this type of video stream, it will switch to the default configuration" + }, + { + "name": "success", + "value": "Success" + }, + { + "name": "record", + "value": "record" + }, + { + "name": "saveButtonNote", + "value": "Allow AVCodecVideo to save captured video to gallery?" + }, + { + "name": "saveButtonCancel", + "value": "Cancel" + }, + { + "name": "saveButtonTitle", + "value": "Confirm the video storage location" + }, + { + "name": "full_width", + "value": "100%" + }, + { + "name": "full_height", + "value": "100%" + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/media/icon.png b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8a354ba394b7575c510700847d6473ecd0b1e740 Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/media/icon.png differ diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/media/setting.png b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/media/setting.png new file mode 100644 index 0000000000000000000000000000000000000000..9e8dd39f05f6572ddf85cd9984f17e5ddd4eeb60 Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/media/setting.png differ diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/profile/main_pages.json b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..a26d8aa4808e92865496ca26ab894e7862885888 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,6 @@ +{ + "src": [ + "pages/Index", + "recorder/Recorder" + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/en_US/element/string.json b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..307d498dfa232c3dbe27af627091534f0a22d3df --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,64 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "This module template implements List functions." + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "AVCodec" + }, + { + "name": "reason", + "value": "label" + }, + { + "name": "playing", + "value": "playing" + }, + { + "name": "play", + "value": "play" + }, + { + "name": "alert", + "value": "None video selected" + }, + { + "name": "alert1", + "value": "This device camera does not support this type of video stream, it will switch to the default configuration" + }, + { + "name": "success", + "value": "Success" + }, + { + "name": "record", + "value": "record" + }, + { + "name": "saveButtonNote", + "value": "Allow AVCodecVideo to save captured video to gallery?" + }, + { + "name": "saveButtonCancel", + "value": "Cancel" + }, + { + "name": "saveButtonTitle", + "value": "Confirm the video storage location" + }, + { + "name": "full_width", + "value": "100%" + }, + { + "name": "full_height", + "value": "100%" + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/main/resources/zh_CN/element/string.json b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..e5110dbd05b74c9e3e8e507b7f6a65d3e31020e3 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,64 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "该模板实现了列表页的功能" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "AVCodec" + }, + { + "name": "reason", + "value": "label" + }, + { + "name": "playing", + "value": "播放中" + }, + { + "name": "play", + "value": "播放" + }, + { + "name": "alert", + "value": "未选择视频!" + }, + { + "name": "alert1", + "value": "本设备相机不支持此类型录像流,将切换至默认配置" + }, + { + "name": "success", + "value": "成功" + }, + { + "name": "record", + "value": "录制" + }, + { + "name": "saveButtonNote", + "value": "是否允许AVCodecVideo保存拍摄的视频到图库?" + }, + { + "name": "saveButtonCancel", + "value": "取消" + }, + { + "name": "saveButtonTitle", + "value": "视频保存位置确认" + }, + { + "name": "full_width", + "value": "100%" + }, + { + "name": "full_height", + "value": "100%" + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/test/Ability.test.ets b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..8c623d142eac0d6131518ae5dd301b91649d98cf --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,50 @@ +/* + * 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 '@ohos.hilog'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/test/List.test.ets b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..c61696f5cb30993f6cdd2866f8941ff18f9542d3 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,20 @@ +/* + * 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 abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testability/TestAbility.ets b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testability/TestAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..a822f2bcb10941cb32b31739270b63f75b094206 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testability/TestAbility.ets @@ -0,0 +1,65 @@ +/* + * 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 UIAbility from '@ohos.app.ability.UIAbility'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import hilog from '@ohos.hilog'; +import { Hypium } from '@ohos/hypium'; +import testsuite from '../test/List.test'; +import window from '@ohos.window'; +import Want from '@ohos.app.ability.Want'; +import AbilityConstant from '@ohos.app.ability.AbilityConstant'; + +export default class TestAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); + hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); + hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:' + JSON.stringify(launchParam) ?? ''); + let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator; + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs; + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments(); + hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); + Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite); + } + + onDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); + windowStage.loadContent('testability/pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', + JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); + } + + onForeground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); + } + + onBackground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testability/pages/Index.ets b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testability/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..2623ec7121a2fdfaea817cfb202467d88bbd2050 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testability/pages/Index.ets @@ -0,0 +1,32 @@ +/* + * 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. + */ + +@Entry +@Component +struct Index { + @State message: string = 'Hello World'; + + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + } + .width('100%') + } + .height('100%') + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts new file mode 100644 index 0000000000000000000000000000000000000000..25607585a98b31d1e14ddad27d9df31acc475efc --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts @@ -0,0 +1,62 @@ +/* + * 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 '@ohos.hilog'; +import TestRunner from '@ohos.application.testRunner'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import Want from '@ohos.app.ability.Want'; + +let abilityDelegator: AbilityDelegatorRegistry.AbilityDelegator | undefined = undefined +let abilityDelegatorArguments: AbilityDelegatorRegistry.AbilityDelegatorArgs | undefined = undefined + +async function onAbilityCreateCallback() { + hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); +} + +async function addAbilityMonitorCallback(err : Error) { + hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); +} + +export default class OpenHarmonyTestRunner implements TestRunner { + constructor() { + } + + onPrepare() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); + } + + async onRun() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + const bundleName = abilityDelegatorArguments.bundleName; + const testAbilityName = 'TestAbility'; + let lMonitor: AbilityDelegatorRegistry.AbilityMonitor = { + abilityName: testAbilityName, + onAbilityCreate: onAbilityCreateCallback, + }; + abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) + const want: Want = { + bundleName: bundleName, + abilityName: testAbilityName + }; + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + abilityDelegator.startAbility(want, (err, data) => { + hilog.info(0x0000, 'testTag', 'startAbility : err : %{public}s', JSON.stringify(err) ?? ''); + hilog.info(0x0000, 'testTag', 'startAbility : data : %{public}s',JSON.stringify(data) ?? ''); + }) + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/module.json5 b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..7fa9cdcda7dba4b52c56a758ecfd75bf52c6ae4a --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/module.json5 @@ -0,0 +1,52 @@ +/* + * 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. + */ + +{ + "module": { + "name": "entry_test", + "type": "feature", + "description": "$string:module_test_desc", + "mainElement": "TestAbility", + "deviceTypes": [ + "default", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:test_pages", + "abilities": [ + { + "name": "TestAbility", + "srcEntry": "./ets/testability/TestAbility.ets", + "description": "$string:TestAbility_desc", + "icon": "$media:icon", + "label": "$string:TestAbility_label", + "exported": true, + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "skills": [ + { + "actions": [ + "action.system.home" + ], + "entities": [ + "entity.system.home" + ] + } + ] + } + ] + } +} diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/element/color.json b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/element/string.json b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..65d8fa5a7cf54aa3943dcd0214f58d1771bc1f6c --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_test_desc", + "value": "test ability description" + }, + { + "name": "TestAbility_desc", + "value": "the test ability" + }, + { + "name": "TestAbility_label", + "value": "test label" + } + ] +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/media/icon.png b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/media/icon.png differ diff --git a/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/profile/test_pages.json b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/profile/test_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..b7e7343cacb32ce982a45e76daad86e435e054fe --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/entry/src/ohosTest/resources/base/profile/test_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "testability/pages/Index" + ] +} diff --git a/code/BasicFeature/Media/AVCodec/hvigor/hvigor-config.json5 b/code/BasicFeature/Media/AVCodec/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..b1f11cd02193cecc52aa65c97c702e8bb13ee9b6 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/hvigor/hvigor-config.json5 @@ -0,0 +1,20 @@ +/* + * 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", + "dependencies": { + } +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/hvigor/hvigor-wrapper.js b/code/BasicFeature/Media/AVCodec/hvigor/hvigor-wrapper.js new file mode 100644 index 0000000000000000000000000000000000000000..372eae8eb4a124095936f9cd78df5c6756746f3f --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/hvigor/hvigor-wrapper.js @@ -0,0 +1 @@ +"use strict";var u=require("path"),D=require("os"),e=require("fs"),t=require("crypto"),r=require("child_process"),n="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},i={},C={},F=n&&n.__importDefault||function(u){return u&&u.__esModule?u:{default:u}};Object.defineProperty(C,"__esModule",{value:!0}),C.maxPathLength=C.isMac=C.isLinux=C.isWindows=void 0;const E=F(D),A="Windows_NT",o="Darwin";function a(){return E.default.type()===A}function c(){return E.default.type()===o}C.isWindows=a,C.isLinux=function(){return"Linux"===E.default.type()},C.isMac=c,C.maxPathLength=function(){return c()?1016:a()?259:4095},function(e){var t=n&&n.__createBinding||(Object.create?function(u,D,e,t){void 0===t&&(t=e);var r=Object.getOwnPropertyDescriptor(D,e);r&&!("get"in r?!D.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return D[e]}}),Object.defineProperty(u,t,r)}:function(u,D,e,t){void 0===t&&(t=e),u[t]=D[e]}),r=n&&n.__setModuleDefault||(Object.create?function(u,D){Object.defineProperty(u,"default",{enumerable:!0,value:D})}:function(u,D){u.default=D}),i=n&&n.__importStar||function(u){if(u&&u.__esModule)return u;var D={};if(null!=u)for(var e in u)"default"!==e&&Object.prototype.hasOwnProperty.call(u,e)&&t(D,u,e);return r(D,u),D};Object.defineProperty(e,"__esModule",{value:!0}),e.WORK_SPACE=e.HVIGOR_PROJECT_WRAPPER_HOME=e.HVIGOR_PROJECT_ROOT_DIR=e.HVIGOR_PROJECT_CACHES_HOME=e.HVIGOR_PNPM_STORE_PATH=e.HVIGOR_WRAPPER_PNPM_SCRIPT_PATH=e.PROJECT_CACHES=e.HVIGOR_WRAPPER_TOOLS_HOME=e.HVIGOR_USER_HOME=e.DEFAULT_PACKAGE_JSON=e.DEFAULT_HVIGOR_CONFIG_JSON_FILE_NAME=e.PNPM=e.HVIGOR=e.NPM_TOOL=e.PNPM_TOOL=e.HVIGOR_ENGINE_PACKAGE_NAME=void 0;const F=i(D),E=i(u),A=C;e.HVIGOR_ENGINE_PACKAGE_NAME="@ohos/hvigor",e.PNPM_TOOL=(0,A.isWindows)()?"pnpm.cmd":"pnpm",e.NPM_TOOL=(0,A.isWindows)()?"npm.cmd":"npm",e.HVIGOR="hvigor",e.PNPM="pnpm",e.DEFAULT_HVIGOR_CONFIG_JSON_FILE_NAME="hvigor-config.json5",e.DEFAULT_PACKAGE_JSON="package.json",e.HVIGOR_USER_HOME=E.resolve(F.homedir(),".hvigor"),e.HVIGOR_WRAPPER_TOOLS_HOME=E.resolve(e.HVIGOR_USER_HOME,"wrapper","tools"),e.PROJECT_CACHES="project_caches",e.HVIGOR_WRAPPER_PNPM_SCRIPT_PATH=E.resolve(e.HVIGOR_WRAPPER_TOOLS_HOME,"node_modules",".bin",e.PNPM_TOOL),e.HVIGOR_PNPM_STORE_PATH=E.resolve(e.HVIGOR_USER_HOME,"caches"),e.HVIGOR_PROJECT_CACHES_HOME=E.resolve(e.HVIGOR_USER_HOME,e.PROJECT_CACHES),e.HVIGOR_PROJECT_ROOT_DIR=process.cwd(),e.HVIGOR_PROJECT_WRAPPER_HOME=E.resolve(e.HVIGOR_PROJECT_ROOT_DIR,e.HVIGOR),e.WORK_SPACE="workspace"}(i);var s={},l={};Object.defineProperty(l,"__esModule",{value:!0}),l.logInfoPrintConsole=l.logErrorAndExit=void 0,l.logErrorAndExit=function(u){u instanceof Error?console.error(u.message):console.error(u),process.exit(-1)},l.logInfoPrintConsole=function(u){console.log(u)};var B=n&&n.__createBinding||(Object.create?function(u,D,e,t){void 0===t&&(t=e);var r=Object.getOwnPropertyDescriptor(D,e);r&&!("get"in r?!D.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return D[e]}}),Object.defineProperty(u,t,r)}:function(u,D,e,t){void 0===t&&(t=e),u[t]=D[e]}),d=n&&n.__setModuleDefault||(Object.create?function(u,D){Object.defineProperty(u,"default",{enumerable:!0,value:D})}:function(u,D){u.default=D}),f=n&&n.__importStar||function(u){if(u&&u.__esModule)return u;var D={};if(null!=u)for(var e in u)"default"!==e&&Object.prototype.hasOwnProperty.call(u,e)&&B(D,u,e);return d(D,u),D};Object.defineProperty(s,"__esModule",{value:!0});var _=s.executeBuild=void 0;const p=f(e),O=f(u),h=l;_=s.executeBuild=function(u){const D=O.resolve(u,"node_modules","@ohos","hvigor","bin","hvigor.js");try{const u=p.realpathSync(D);require(u)}catch(e){(0,h.logErrorAndExit)(`Error: ENOENT: no such file ${D},delete ${u} and retry.`)}};var P={},v={};!function(u){var D=n&&n.__importDefault||function(u){return u&&u.__esModule?u:{default:u}};Object.defineProperty(u,"__esModule",{value:!0}),u.hashFile=u.hash=u.createHash=void 0;const r=D(t),i=D(e);u.createHash=(u="MD5")=>r.default.createHash(u);u.hash=(D,e)=>(0,u.createHash)(e).update(D).digest("hex");u.hashFile=(D,e)=>{if(i.default.existsSync(D))return(0,u.hash)(i.default.readFileSync(D,"utf-8"),e)}}(v);var g={},m={},R={};Object.defineProperty(R,"__esModule",{value:!0}),R.Unicode=void 0;class y{}R.Unicode=y,y.SPACE_SEPARATOR=/[\u1680\u2000-\u200A\u202F\u205F\u3000]/,y.ID_START=/[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/,y.ID_CONTINUE=/[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/,Object.defineProperty(m,"__esModule",{value:!0}),m.JudgeUtil=void 0;const I=R;m.JudgeUtil=class{static isIgnoreChar(u){return"string"==typeof u&&("\t"===u||"\v"===u||"\f"===u||" "===u||" "===u||"\ufeff"===u||"\n"===u||"\r"===u||"\u2028"===u||"\u2029"===u)}static isSpaceSeparator(u){return"string"==typeof u&&I.Unicode.SPACE_SEPARATOR.test(u)}static isIdStartChar(u){return"string"==typeof u&&(u>="a"&&u<="z"||u>="A"&&u<="Z"||"$"===u||"_"===u||I.Unicode.ID_START.test(u))}static isIdContinueChar(u){return"string"==typeof u&&(u>="a"&&u<="z"||u>="A"&&u<="Z"||u>="0"&&u<="9"||"$"===u||"_"===u||"‌"===u||"‍"===u||I.Unicode.ID_CONTINUE.test(u))}static isDigitWithoutZero(u){return/[1-9]/.test(u)}static isDigit(u){return"string"==typeof u&&/[0-9]/.test(u)}static isHexDigit(u){return"string"==typeof u&&/[0-9A-Fa-f]/.test(u)}};var N=n&&n.__importDefault||function(u){return u&&u.__esModule?u:{default:u}};Object.defineProperty(g,"__esModule",{value:!0}),g.parseJsonText=g.parseJsonFile=void 0;const b=N(e),S=N(D),w=N(u),H=m;var x;!function(u){u[u.Char=0]="Char",u[u.EOF=1]="EOF",u[u.Identifier=2]="Identifier"}(x||(x={}));let M,T,V,G,j,J,W="start",U=[],L=0,$=1,k=0,K=!1,z="default",q="'",Z=1;function X(u,D=!1){T=String(u),W="start",U=[],L=0,$=1,k=0,G=void 0,K=D;do{M=Q(),nu[W]()}while("eof"!==M.type);return G}function Q(){for(z="default",j="",q="'",Z=1;;){J=Y();const u=Du[z]();if(u)return u}}function Y(){if(T[L])return String.fromCodePoint(T.codePointAt(L))}function uu(){const u=Y();return"\n"===u?($++,k=0):u?k+=u.length:k++,u&&(L+=u.length),u}g.parseJsonFile=function(u,D=!1,e="utf-8"){const t=b.default.readFileSync(w.default.resolve(u),{encoding:e});try{return X(t,D)}catch(D){if(D instanceof SyntaxError){const e=D.message.split("at");if(2===e.length)throw new Error(`${e[0].trim()}${S.default.EOL}\t at ${u}:${e[1].trim()}`)}throw new Error(`${u} is not in valid JSON/JSON5 format.`)}},g.parseJsonText=X;const Du={default(){switch(J){case"/":return uu(),void(z="comment");case void 0:return uu(),eu("eof")}if(!H.JudgeUtil.isIgnoreChar(J)&&!H.JudgeUtil.isSpaceSeparator(J))return Du[W]();uu()},start(){z="value"},beforePropertyName(){switch(J){case"$":case"_":return j=uu(),void(z="identifierName");case"\\":return uu(),void(z="identifierNameStartEscape");case"}":return eu("punctuator",uu());case'"':case"'":return q=J,uu(),void(z="string")}if(H.JudgeUtil.isIdStartChar(J))return j+=uu(),void(z="identifierName");throw Eu(x.Char,uu())},afterPropertyName(){if(":"===J)return eu("punctuator",uu());throw Eu(x.Char,uu())},beforePropertyValue(){z="value"},afterPropertyValue(){switch(J){case",":case"}":return eu("punctuator",uu())}throw Eu(x.Char,uu())},beforeArrayValue(){if("]"===J)return eu("punctuator",uu());z="value"},afterArrayValue(){switch(J){case",":case"]":return eu("punctuator",uu())}throw Eu(x.Char,uu())},end(){throw Eu(x.Char,uu())},comment(){switch(J){case"*":return uu(),void(z="multiLineComment");case"/":return uu(),void(z="singleLineComment")}throw Eu(x.Char,uu())},multiLineComment(){switch(J){case"*":return uu(),void(z="multiLineCommentAsterisk");case void 0:throw Eu(x.Char,uu())}uu()},multiLineCommentAsterisk(){switch(J){case"*":return void uu();case"/":return uu(),void(z="default");case void 0:throw Eu(x.Char,uu())}uu(),z="multiLineComment"},singleLineComment(){switch(J){case"\n":case"\r":case"\u2028":case"\u2029":return uu(),void(z="default");case void 0:return uu(),eu("eof")}uu()},value(){switch(J){case"{":case"[":return eu("punctuator",uu());case"n":return uu(),tu("ull"),eu("null",null);case"t":return uu(),tu("rue"),eu("boolean",!0);case"f":return uu(),tu("alse"),eu("boolean",!1);case"-":case"+":return"-"===uu()&&(Z=-1),void(z="numerical");case".":case"0":case"I":case"N":return void(z="numerical");case'"':case"'":return q=J,uu(),j="",void(z="string")}if(void 0===J||!H.JudgeUtil.isDigitWithoutZero(J))throw Eu(x.Char,uu());z="numerical"},numerical(){switch(J){case".":return j=uu(),void(z="decimalPointLeading");case"0":return j=uu(),void(z="zero");case"I":return uu(),tu("nfinity"),eu("numeric",Z*(1/0));case"N":return uu(),tu("aN"),eu("numeric",NaN)}if(void 0!==J&&H.JudgeUtil.isDigitWithoutZero(J))return j=uu(),void(z="decimalInteger");throw Eu(x.Char,uu())},zero(){switch(J){case".":case"e":case"E":return void(z="decimal");case"x":case"X":return j+=uu(),void(z="hexadecimal")}return eu("numeric",0)},decimalInteger(){switch(J){case".":case"e":case"E":return void(z="decimal")}if(!H.JudgeUtil.isDigit(J))return eu("numeric",Z*Number(j));j+=uu()},decimal(){switch(J){case".":j+=uu(),z="decimalFraction";break;case"e":case"E":j+=uu(),z="decimalExponent"}},decimalPointLeading(){if(H.JudgeUtil.isDigit(J))return j+=uu(),void(z="decimalFraction");throw Eu(x.Char,uu())},decimalFraction(){switch(J){case"e":case"E":return j+=uu(),void(z="decimalExponent")}if(!H.JudgeUtil.isDigit(J))return eu("numeric",Z*Number(j));j+=uu()},decimalExponent(){switch(J){case"+":case"-":return j+=uu(),void(z="decimalExponentSign")}if(H.JudgeUtil.isDigit(J))return j+=uu(),void(z="decimalExponentInteger");throw Eu(x.Char,uu())},decimalExponentSign(){if(H.JudgeUtil.isDigit(J))return j+=uu(),void(z="decimalExponentInteger");throw Eu(x.Char,uu())},decimalExponentInteger(){if(!H.JudgeUtil.isDigit(J))return eu("numeric",Z*Number(j));j+=uu()},hexadecimal(){if(H.JudgeUtil.isHexDigit(J))return j+=uu(),void(z="hexadecimalInteger");throw Eu(x.Char,uu())},hexadecimalInteger(){if(!H.JudgeUtil.isHexDigit(J))return eu("numeric",Z*Number(j));j+=uu()},identifierNameStartEscape(){if("u"!==J)throw Eu(x.Char,uu());uu();const u=ru();switch(u){case"$":case"_":break;default:if(!H.JudgeUtil.isIdStartChar(u))throw Eu(x.Identifier)}j+=u,z="identifierName"},identifierName(){switch(J){case"$":case"_":case"‌":case"‍":return void(j+=uu());case"\\":return uu(),void(z="identifierNameEscape")}if(!H.JudgeUtil.isIdContinueChar(J))return eu("identifier",j);j+=uu()},identifierNameEscape(){if("u"!==J)throw Eu(x.Char,uu());uu();const u=ru();switch(u){case"$":case"_":case"‌":case"‍":break;default:if(!H.JudgeUtil.isIdContinueChar(u))throw Eu(x.Identifier)}j+=u,z="identifierName"},string(){switch(J){case"\\":return uu(),void(j+=function(){const u=Y(),D=function(){switch(Y()){case"b":return uu(),"\b";case"f":return uu(),"\f";case"n":return uu(),"\n";case"r":return uu(),"\r";case"t":return uu(),"\t";case"v":return uu(),"\v"}return}();if(D)return D;switch(u){case"0":if(uu(),H.JudgeUtil.isDigit(Y()))throw Eu(x.Char,uu());return"\0";case"x":return uu(),function(){let u="",D=Y();if(!H.JudgeUtil.isHexDigit(D))throw Eu(x.Char,uu());if(u+=uu(),D=Y(),!H.JudgeUtil.isHexDigit(D))throw Eu(x.Char,uu());return u+=uu(),String.fromCodePoint(parseInt(u,16))}();case"u":return uu(),ru();case"\n":case"\u2028":case"\u2029":return uu(),"";case"\r":return uu(),"\n"===Y()&&uu(),""}if(void 0===u||H.JudgeUtil.isDigitWithoutZero(u))throw Eu(x.Char,uu());return uu()}());case'"':case"'":if(J===q){const u=eu("string",j);return uu(),u}return void(j+=uu());case"\n":case"\r":case void 0:throw Eu(x.Char,uu());case"\u2028":case"\u2029":!function(u){console.warn(`JSON5: '${Fu(u)}' in strings is not valid ECMAScript; consider escaping.`)}(J)}j+=uu()}};function eu(u,D){return{type:u,value:D,line:$,column:k}}function tu(u){for(const D of u){if(Y()!==D)throw Eu(x.Char,uu());uu()}}function ru(){let u="",D=4;for(;D-- >0;){const D=Y();if(!H.JudgeUtil.isHexDigit(D))throw Eu(x.Char,uu());u+=uu()}return String.fromCodePoint(parseInt(u,16))}const nu={start(){if("eof"===M.type)throw Eu(x.EOF);iu()},beforePropertyName(){switch(M.type){case"identifier":case"string":return V=M.value,void(W="afterPropertyName");case"punctuator":return void Cu();case"eof":throw Eu(x.EOF)}},afterPropertyName(){if("eof"===M.type)throw Eu(x.EOF);W="beforePropertyValue"},beforePropertyValue(){if("eof"===M.type)throw Eu(x.EOF);iu()},afterPropertyValue(){if("eof"===M.type)throw Eu(x.EOF);switch(M.value){case",":return void(W="beforePropertyName");case"}":Cu()}},beforeArrayValue(){if("eof"===M.type)throw Eu(x.EOF);"punctuator"!==M.type||"]"!==M.value?iu():Cu()},afterArrayValue(){if("eof"===M.type)throw Eu(x.EOF);switch(M.value){case",":return void(W="beforeArrayValue");case"]":Cu()}},end(){}};function iu(){const u=function(){let u;switch(M.type){case"punctuator":switch(M.value){case"{":u={};break;case"[":u=[]}break;case"null":case"boolean":case"numeric":case"string":u=M.value}return u}();if(K&&"object"==typeof u&&(u._line=$,u._column=k),void 0===G)G=u;else{const D=U[U.length-1];Array.isArray(D)?K&&"object"!=typeof u?D.push({value:u,_line:$,_column:k}):D.push(u):D[V]=K&&"object"!=typeof u?{value:u,_line:$,_column:k}:u}!function(u){if(u&&"object"==typeof u)U.push(u),W=Array.isArray(u)?"beforeArrayValue":"beforePropertyName";else{const u=U[U.length-1];W=u?Array.isArray(u)?"afterArrayValue":"afterPropertyValue":"end"}}(u)}function Cu(){U.pop();const u=U[U.length-1];W=u?Array.isArray(u)?"afterArrayValue":"afterPropertyValue":"end"}function Fu(u){const D={"'":"\\'",'"':'\\"',"\\":"\\\\","\b":"\\b","\f":"\\f","\n":"\\n","\r":"\\r","\t":"\\t","\v":"\\v","\0":"\\0","\u2028":"\\u2028","\u2029":"\\u2029"};if(D[u])return D[u];if(u<" "){const D=u.charCodeAt(0).toString(16);return`\\x${`00${D}`.substring(D.length)}`}return u}function Eu(u,D){let e="";switch(u){case x.Char:e=void 0===D?`JSON5: invalid end of input at ${$}:${k}`:`JSON5: invalid character '${Fu(D)}' at ${$}:${k}`;break;case x.EOF:e=`JSON5: invalid end of input at ${$}:${k}`;break;case x.Identifier:k-=5,e=`JSON5: invalid identifier character at ${$}:${k}`}const t=new Au(e);return t.lineNumber=$,t.columnNumber=k,t}class Au extends SyntaxError{}var ou={},au=n&&n.__createBinding||(Object.create?function(u,D,e,t){void 0===t&&(t=e);var r=Object.getOwnPropertyDescriptor(D,e);r&&!("get"in r?!D.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return D[e]}}),Object.defineProperty(u,t,r)}:function(u,D,e,t){void 0===t&&(t=e),u[t]=D[e]}),cu=n&&n.__setModuleDefault||(Object.create?function(u,D){Object.defineProperty(u,"default",{enumerable:!0,value:D})}:function(u,D){u.default=D}),su=n&&n.__importStar||function(u){if(u&&u.__esModule)return u;var D={};if(null!=u)for(var e in u)"default"!==e&&Object.prototype.hasOwnProperty.call(u,e)&&au(D,u,e);return cu(D,u),D},lu=n&&n.__importDefault||function(u){return u&&u.__esModule?u:{default:u}};Object.defineProperty(ou,"__esModule",{value:!0}),ou.isFileExists=ou.offlinePluginConversion=ou.executeCommand=ou.getNpmPath=ou.hasNpmPackInPaths=void 0;const Bu=r,du=lu(e),fu=su(u),_u=i,pu=l;ou.hasNpmPackInPaths=function(u,D){try{return require.resolve(u,{paths:[...D]}),!0}catch(u){return!1}},ou.getNpmPath=function(){const u=process.execPath;return fu.join(fu.dirname(u),_u.NPM_TOOL)},ou.executeCommand=function(u,D,e){0!==(0,Bu.spawnSync)(u,D,e).status&&(0,pu.logErrorAndExit)(`Error: ${u} ${D} execute failed.See above for details.`)},ou.offlinePluginConversion=function(u,D){return D.startsWith("file:")||D.endsWith(".tgz")?fu.resolve(u,_u.HVIGOR,D.replace("file:","")):D},ou.isFileExists=function(u){return du.default.existsSync(u)&&du.default.statSync(u).isFile()};var Ou=n&&n.__createBinding||(Object.create?function(u,D,e,t){void 0===t&&(t=e);var r=Object.getOwnPropertyDescriptor(D,e);r&&!("get"in r?!D.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return D[e]}}),Object.defineProperty(u,t,r)}:function(u,D,e,t){void 0===t&&(t=e),u[t]=D[e]}),hu=n&&n.__setModuleDefault||(Object.create?function(u,D){Object.defineProperty(u,"default",{enumerable:!0,value:D})}:function(u,D){u.default=D}),Pu=n&&n.__importStar||function(u){if(u&&u.__esModule)return u;var D={};if(null!=u)for(var e in u)"default"!==e&&Object.prototype.hasOwnProperty.call(u,e)&&Ou(D,u,e);return hu(D,u),D},vu=n&&n.__importDefault||function(u){return u&&u.__esModule?u:{default:u}};Object.defineProperty(P,"__esModule",{value:!0});var gu=P.initProjectWorkSpace=void 0;const mu=Pu(e),Ru=vu(D),yu=Pu(u),Iu=v,Nu=i,bu=g,Su=l,wu=ou;let Hu,xu,Mu;function Tu(u,D,e){return void 0!==e.dependencies&&(0,wu.offlinePluginConversion)(Nu.HVIGOR_PROJECT_ROOT_DIR,D.dependencies[u])===yu.normalize(e.dependencies[u])}function Vu(){const u=yu.join(Mu,Nu.WORK_SPACE);if((0,Su.logInfoPrintConsole)("Hvigor cleaning..."),!mu.existsSync(u))return;const D=mu.readdirSync(u);if(!D||0===D.length)return;const e=yu.resolve(Mu,"node_modules","@ohos","hvigor","bin","hvigor.js");mu.existsSync(e)&&(0,wu.executeCommand)(process.argv[0],[e,"--stop-daemon"],{});try{D.forEach((D=>{mu.rmSync(yu.resolve(u,D),{recursive:!0})}))}catch(D){(0,Su.logErrorAndExit)(`The hvigor build tool cannot be installed. Please manually clear the workspace directory and synchronize the project again.\n\n Workspace Path: ${u}.`)}}gu=P.initProjectWorkSpace=function(){if(Hu=function(){const u=yu.resolve(Nu.HVIGOR_PROJECT_WRAPPER_HOME,Nu.DEFAULT_HVIGOR_CONFIG_JSON_FILE_NAME);mu.existsSync(u)||(0,Su.logErrorAndExit)(`Error: Hvigor config file ${u} does not exist.`);return(0,bu.parseJsonFile)(u)}(),Mu=function(u){let D;D=function(u){let D=u.hvigorVersion;if(D.startsWith("file:")||D.endsWith(".tgz"))return!1;const e=u.dependencies,t=Object.getOwnPropertyNames(e);for(const u of t){const D=e[u];if(D.startsWith("file:")||D.endsWith(".tgz"))return!1}if(1===t.length&&"@ohos/hvigor-ohos-plugin"===t[0])return D>"2.5.0";return!1}(u)?function(u){let D=`${Nu.HVIGOR_ENGINE_PACKAGE_NAME}@${u.hvigorVersion}`;const e=u.dependencies;if(e){Object.getOwnPropertyNames(e).sort().forEach((u=>{D+=`,${u}@${e[u]}`}))}return(0,Iu.hash)(D)}(u):(0,Iu.hash)(process.cwd());return yu.resolve(Ru.default.homedir(),".hvigor","project_caches",D)}(Hu),xu=function(){const u=yu.resolve(Mu,Nu.WORK_SPACE,Nu.DEFAULT_PACKAGE_JSON);return mu.existsSync(u)?(0,bu.parseJsonFile)(u):{dependencies:{}}}(),!(0,wu.hasNpmPackInPaths)(Nu.HVIGOR_ENGINE_PACKAGE_NAME,[yu.join(Mu,Nu.WORK_SPACE)])||(0,wu.offlinePluginConversion)(Nu.HVIGOR_PROJECT_ROOT_DIR,Hu.hvigorVersion)!==xu.dependencies[Nu.HVIGOR_ENGINE_PACKAGE_NAME]||!function(){function u(u){const D=null==u?void 0:u.dependencies;return void 0===D?0:Object.getOwnPropertyNames(D).length}const D=u(Hu),e=u(xu);if(D+1!==e)return!1;for(const u in null==Hu?void 0:Hu.dependencies)if(!(0,wu.hasNpmPackInPaths)(u,[yu.join(Mu,Nu.WORK_SPACE)])||!Tu(u,Hu,xu))return!1;return!0}()){Vu();try{!function(){(0,Su.logInfoPrintConsole)("Hvigor installing...");for(const u in Hu.dependencies)Hu.dependencies[u]&&(Hu.dependencies[u]=(0,wu.offlinePluginConversion)(Nu.HVIGOR_PROJECT_ROOT_DIR,Hu.dependencies[u]));const u={dependencies:{...Hu.dependencies}};u.dependencies[Nu.HVIGOR_ENGINE_PACKAGE_NAME]=(0,wu.offlinePluginConversion)(Nu.HVIGOR_PROJECT_ROOT_DIR,Hu.hvigorVersion);const D=yu.join(Mu,Nu.WORK_SPACE);try{mu.mkdirSync(D,{recursive:!0});const e=yu.resolve(D,Nu.DEFAULT_PACKAGE_JSON);mu.writeFileSync(e,JSON.stringify(u))}catch(u){(0,Su.logErrorAndExit)(u)}(function(){const u=["config","set","store-dir",Nu.HVIGOR_PNPM_STORE_PATH],D={cwd:yu.join(Mu,Nu.WORK_SPACE),stdio:["inherit","inherit","inherit"]};(0,wu.executeCommand)(Nu.HVIGOR_WRAPPER_PNPM_SCRIPT_PATH,u,D)})(),function(){const u=["install"],D={cwd:yu.join(Mu,Nu.WORK_SPACE),stdio:["inherit","inherit","inherit"]};(0,wu.executeCommand)(Nu.HVIGOR_WRAPPER_PNPM_SCRIPT_PATH,u,D)}(),(0,Su.logInfoPrintConsole)("Hvigor install success.")}()}catch(u){Vu()}}return Mu};var Gu={};!function(t){var C=n&&n.__createBinding||(Object.create?function(u,D,e,t){void 0===t&&(t=e);var r=Object.getOwnPropertyDescriptor(D,e);r&&!("get"in r?!D.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return D[e]}}),Object.defineProperty(u,t,r)}:function(u,D,e,t){void 0===t&&(t=e),u[t]=D[e]}),F=n&&n.__setModuleDefault||(Object.create?function(u,D){Object.defineProperty(u,"default",{enumerable:!0,value:D})}:function(u,D){u.default=D}),E=n&&n.__importStar||function(u){if(u&&u.__esModule)return u;var D={};if(null!=u)for(var e in u)"default"!==e&&Object.prototype.hasOwnProperty.call(u,e)&&C(D,u,e);return F(D,u),D},A=n&&n.__importDefault||function(u){return u&&u.__esModule?u:{default:u}};Object.defineProperty(t,"__esModule",{value:!0}),t.executeInstallPnpm=t.isPnpmInstalled=t.environmentHandler=t.checkNpmConifg=t.PNPM_VERSION=void 0;const o=r,a=E(e),c=A(D),s=E(u),B=i,d=l,f=ou;t.PNPM_VERSION="7.30.0",t.checkNpmConifg=function(){const u=s.resolve(B.HVIGOR_PROJECT_ROOT_DIR,".npmrc"),D=s.resolve(c.default.homedir(),".npmrc");if((0,f.isFileExists)(u)||(0,f.isFileExists)(D))return;const e=(0,f.getNpmPath)(),t=(0,o.spawnSync)(e,["config","get","prefix"],{cwd:B.HVIGOR_PROJECT_ROOT_DIR});if(0!==t.status||!t.stdout)return void(0,d.logErrorAndExit)("Error: The hvigor depends on the npmrc file. Configure the npmrc file first.");const r=s.resolve(`${t.stdout}`.replace(/[\r\n]/gi,""),".npmrc");(0,f.isFileExists)(r)||(0,d.logErrorAndExit)("Error: The hvigor depends on the npmrc file. Configure the npmrc file first.")},t.environmentHandler=function(){process.env["npm_config_update-notifier"]="false"},t.isPnpmInstalled=function(){return!!a.existsSync(B.HVIGOR_WRAPPER_PNPM_SCRIPT_PATH)&&(0,f.hasNpmPackInPaths)("pnpm",[B.HVIGOR_WRAPPER_TOOLS_HOME])},t.executeInstallPnpm=function(){(0,d.logInfoPrintConsole)(`Installing pnpm@${t.PNPM_VERSION}...`);const u=(0,f.getNpmPath)();!function(){const u=s.resolve(B.HVIGOR_WRAPPER_TOOLS_HOME,B.DEFAULT_PACKAGE_JSON);try{a.existsSync(B.HVIGOR_WRAPPER_TOOLS_HOME)||a.mkdirSync(B.HVIGOR_WRAPPER_TOOLS_HOME,{recursive:!0});const D={dependencies:{}};D.dependencies[B.PNPM]=t.PNPM_VERSION,a.writeFileSync(u,JSON.stringify(D))}catch(D){(0,d.logErrorAndExit)(`Error: EPERM: operation not permitted,create ${u} failed.`)}}(),(0,f.executeCommand)(u,["install","pnpm"],{cwd:B.HVIGOR_WRAPPER_TOOLS_HOME,stdio:["inherit","inherit","inherit"],env:process.env}),(0,d.logInfoPrintConsole)("Pnpm install success.")}}(Gu),function(){Gu.checkNpmConifg(),Gu.environmentHandler(),Gu.isPnpmInstalled()||Gu.executeInstallPnpm();const D=gu();_(u.join(D,i.WORK_SPACE))}(); \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/hvigorfile.ts b/code/BasicFeature/Media/AVCodec/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..cd526255e06ee9f18b460930ac095ec26aae65e4 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/hvigorfile.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { appTasks } from '@ohos/hvigor-ohos-plugin'; \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/oh-package.json5 b/code/BasicFeature/Media/AVCodec/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..3d1bc17aa152a3ac1496b3dc8f7f2d5807d7e009 --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/oh-package.json5 @@ -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. + */ + +{ + "modelVersion": "5.0.0", + "license": "", + "devDependencies": { + }, + "author": "", + "name": "avcodecsample", + "description": "Please describe the basic information.", + "main": "", + "version": "1.0.0", + "dependencies": {} +} \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/ohosTest.md b/code/BasicFeature/Media/AVCodec/ohosTest.md new file mode 100644 index 0000000000000000000000000000000000000000..70b7c634d0c1f8de25cc422f6f130f6a366a2c4a --- /dev/null +++ b/code/BasicFeature/Media/AVCodec/ohosTest.md @@ -0,0 +1,31 @@ +# AVCodecSample测试用例归档 + +### 用例表 + +| 测试功能 | 预置条件 | 输入 | 预期输出 | 是否自动 | 测试结果 | +|:-----------------------|:-----------------------------------------|:------------------------|:------------------------------------------------------------------------|:-----|:-----| +| 首次拉起应用 | 设备正常运行 | 无 | 弹出相机权限申请 | 是 | Pass | +| 主页从上往下第一个 设置 按钮点击 | 位于主页 | 点击第一个设置按钮 | 弹出滑动选择器弹窗 | 否 | Pass | +| 第一个设置触发的滑动选择器弹窗 | 位于主页,已点击第一个设置按钮 | 滑动选择参数后点击确定 | 主页弹出"成功"文字弹窗 | 否 | Pass | +| 播放按钮点击 | 位于主页 | 点击播放按钮 | 弹出滑动选择器弹窗 | 否 | Pass | +| 播放按钮触发的滑动选择器弹窗 | 位于主页,已点击播放按钮 | 滑动选择参数后点击确定 | 根据用户的选择,弹出对应的文件选择模块 | 否 | Pass | +| 图库视频Picker | 位于主页,点击主页播放后,选择“从图库选取”后,点击确定 | 选取一个视频后,勾选原图后,点击完成 | 主页开始播放用户选取的视频/视频含音频,播放按钮文字变为“播放中”,同时主页“倍速”按钮变为可点击状态,主页其余按钮变为不可点击状态 | 否 | Pass | +| 图库视频Picker | 位于主页,点击主页播放后,选择“从图库选取”后,点击确定 | 不选取视频,直接返回 | 主页弹出“未选择视频!”文字弹窗 | 否 | Pass | +| 文件管理FilePicker | 位于主页,点击主页播放后,选择“从文件管理选取”后,点击确定 | 选取一个视频或音频文件后,勾选原图后,点击完成 | 主页开始播放用户选取的视频/视频含音频/音频,播放按钮文字变为“播放中”,同时主页“倍速”按钮变为可点击状态,主页其余按钮变为不可点击状态 | 否 | Pass | +| 文件管理FilePicker | 位于主页,点击主页播放后,选择“从文件管理选取”后,点击确定 | 不选取视频,直接返回 | 主页弹出“未选择视频!”文字弹窗 | 否 | Pass | +| 播放画面自动裁剪 | 位于主页,点击主页播放后,选择文件后,点击确定,开始播放后 | 无 | 横竖屏视频,保持视频原有的宽高比例,缩放至其中一边碰到手机任意侧边 | 是 | Pass | +| 倍速播放按钮触发倍速播放 | 位于主页,点击主页播放后,选择文件后,点击确定,开始播放后 | 点击倍速按钮 | 主页弹出“播放时长按播放窗口也可倍速播放”文字弹窗,和速度选择器弹窗 | 否 | Pass | +| 倍速播放速度选择器弹窗 | 位于主页,点击主页播放后,选择文件后,点击确定,开始播放后,点击倍速按钮后 | 滑动选择参数后点击确定 | 用户选择了什么参数,主页就弹出对应的文字弹窗,比如,用户选择了“X3”并点击确定后,主页就会弹出“X3”文字弹窗,同时播放速度也变成对应的倍速 | 否 | Pass | +| 播放中长按播放窗口触发倍速播放 | 位于主页,点击主页播放后,选择文件后,点击确定,开始播放后 | 长按播放窗口 | 主页闪烁文字弹窗“X2”,播放速度变为两倍 | 否 | Pass | +| 播放中松开长按播放窗口的手指后,恢复原速播放 | 位于主页,点击主页播放后,选择文件后,点击确定,开始播放后,长按播放窗口时 | 松开手指 | 主页弹出文字弹窗“X1”,播放速度变为原速 | 否 | Pass | +| 播放结束 | 位于主页,点击主页播放后,选择文件后,点击确定,开始播放后,播放结束后 | 无 | 主页倍速按钮变为不可点击状态,其余按钮变为可点击状态,主页播放窗口保留视频最后一帧画面,播放按钮文字由“播放中”变回“播放” | 是 | Pass | +| 主页从上往下第二个 设置 按钮点击 | 位于主页 | 点击第二个设置按钮 | 弹出滑动选择器弹窗 | 否 | Pass | +| 第二个设置触发的滑动选择器弹窗 | 已允许应用访问相机权限申请,位于主页,已点击第二个设置按钮 | 滑动选择参数后点击确定 | 若设备支持应用使用此参数的相机,会弹出“成功”的文字弹窗;若不支持,则弹出“本设备相机不支持此类型录像流,将切换至默认配置” | 否 | Pass | +| 第二个设置触发的滑动选择器弹窗 | 已允许应用访问相机权限申请,位于主页,已点击第二个设置按钮 | 滑动选择参数后点击确定 | 若设备支持应用使用此参数的相机,会弹出“成功”的文字弹窗;若不支持,则弹出“本设备相机不支持此类型录像流,将切换至默认配置” | 否 | Pass | +| 录制按钮点击 | 已允许应用访问相机权限申请,位于主页 | 点击录制按钮 | 弹出“视频保存位置确认”弹窗 | 否 | Pass | +| 首次视频保存位置确认弹窗 | 已允许应用访问相机权限申请,位于主页,已点击录制按钮 | 点击保存按钮 | 弹出安全保存图片和视频弹窗 | 否 | Pass | +| 安全保存图片和视频弹窗 | 已允许应用访问相机权限申请,位于主页,已点击录制按钮,已点击视频保存位置确认弹窗 | 点击允许按钮 | 点击允许后,跳转页面至录像页面,录像页面预览正常正常,同时弹出“成功”文字弹窗 | 否 | Pass | +| 视频保存位置确认弹窗 | 已允许应用访问相机权限申请,位于主页,已点击录制按钮 | 点击保存按钮 | 点击允许后,跳转页面至录像页面,录像页面预览正常正常,同时弹出“成功”文字弹窗 | 否 | Pass | +| 开始录制按钮点击 | 已允许应用访问相机权限申请,位于录制页面 | 点击开始录制 | 点击开始录制后,按钮文字变成“停止录制”,按钮状态变成不可点击状态,1.3秒后,按钮状态变回可点击状态;同时页面上方出现计时器,计时器开始记时 | 否 | Pass | +| 停止录制按钮点击 | 已允许应用访问相机权限申请,位于录制页面,已点击开始录制 | 点击停止录制 | 点击停止录制后,页面跳转回主页,图库中出现录制的视频 | 否 | Pass | +| 相机缩放 | 已允许应用访问相机权限申请,位于录制页面 | 双指捏合放大 | 预览画面随着双指捏合放大,同步缩放 | 否 | Pass | \ No newline at end of file diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img.png b/code/BasicFeature/Media/AVCodec/screenshots/img.png new file mode 100644 index 0000000000000000000000000000000000000000..33a97e3ec68a2d685549d81e68f3493d26c7dada Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img.png differ diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img_1.png b/code/BasicFeature/Media/AVCodec/screenshots/img_1.png new file mode 100644 index 0000000000000000000000000000000000000000..a5ccecb6777889de682b2ba0d11acb8c3135aa4e Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img_1.png differ diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img_2.png b/code/BasicFeature/Media/AVCodec/screenshots/img_2.png new file mode 100644 index 0000000000000000000000000000000000000000..aaba24caad6be2d97bcb0e60505b80ef21c397bd Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img_2.png differ diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img_4.png b/code/BasicFeature/Media/AVCodec/screenshots/img_4.png new file mode 100644 index 0000000000000000000000000000000000000000..4f694bbfb7e0969087f7ddb26d669313415ca29b Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img_4.png differ diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img_5.png b/code/BasicFeature/Media/AVCodec/screenshots/img_5.png new file mode 100644 index 0000000000000000000000000000000000000000..d53bb875a99982bce58ea14789089a3c29eb1427 Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img_5.png differ diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img_6.png b/code/BasicFeature/Media/AVCodec/screenshots/img_6.png new file mode 100644 index 0000000000000000000000000000000000000000..c902d700778075a455188088e7d060646bb3c4de Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img_6.png differ diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img_7.png b/code/BasicFeature/Media/AVCodec/screenshots/img_7.png new file mode 100644 index 0000000000000000000000000000000000000000..daf0a2859d06c907737010e3ca70072bfffb9262 Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img_7.png differ diff --git a/code/BasicFeature/Media/AVCodec/screenshots/img_8.png b/code/BasicFeature/Media/AVCodec/screenshots/img_8.png new file mode 100644 index 0000000000000000000000000000000000000000..50916ab681a386db769a0747a1e9754c05e969bb Binary files /dev/null and b/code/BasicFeature/Media/AVCodec/screenshots/img_8.png differ diff --git "a/code/BasicFeature/Media/AVCodec/screenshots/\345\275\225\345\210\266_\345\274\200\345\247\213\345\275\225\345\210\266.jpeg" "b/code/BasicFeature/Media/AVCodec/screenshots/\345\275\225\345\210\266_\345\274\200\345\247\213\345\275\225\345\210\266.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..083002ad9f13fe0fccfae3ba672ba4048601688c Binary files /dev/null and "b/code/BasicFeature/Media/AVCodec/screenshots/\345\275\225\345\210\266_\345\274\200\345\247\213\345\275\225\345\210\266.jpeg" differ diff --git "a/code/BasicFeature/Media/AVCodec/screenshots/\345\275\225\345\210\266_\351\200\211\346\213\251\347\233\270\346\234\272\345\210\206\350\276\250\347\216\207.jpeg" "b/code/BasicFeature/Media/AVCodec/screenshots/\345\275\225\345\210\266_\351\200\211\346\213\251\347\233\270\346\234\272\345\210\206\350\276\250\347\216\207.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..180568621b41c96539e7ec8fd0eab25540889b61 Binary files /dev/null and "b/code/BasicFeature/Media/AVCodec/screenshots/\345\275\225\345\210\266_\351\200\211\346\213\251\347\233\270\346\234\272\345\210\206\350\276\250\347\216\207.jpeg" differ diff --git "a/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\345\200\215\351\200\237.jpeg" "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\345\200\215\351\200\237.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..693175062093560fac13743f5d4ba073870ec233 Binary files /dev/null and "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\345\200\215\351\200\237.jpeg" differ diff --git "a/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\346\250\241\345\274\217\351\200\211\346\213\251.jpeg" "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\346\250\241\345\274\217\351\200\211\346\213\251.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..64f2fd62cff08c3f5ea24290fee3639bdc1e433e Binary files /dev/null and "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\346\250\241\345\274\217\351\200\211\346\213\251.jpeg" differ diff --git "a/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\346\250\252\345\261\217.jpeg" "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\346\250\252\345\261\217.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..1127a56e245a9078210ed18783c8085e02b49b54 Binary files /dev/null and "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\346\250\252\345\261\217.jpeg" differ diff --git "a/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\347\253\226\345\261\217.jpeg" "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\347\253\226\345\261\217.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..cc80f29a16ad5689bf9087290da524b2a71cc959 Binary files /dev/null and "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\347\253\226\345\261\217.jpeg" differ diff --git "a/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\351\200\211\346\213\251\346\222\255\346\224\276\350\267\257\345\276\204.jpeg" "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\351\200\211\346\213\251\346\222\255\346\224\276\350\267\257\345\276\204.jpeg" new file mode 100644 index 0000000000000000000000000000000000000000..a3b5f26f44c577957721ef15bcae4b4fcc5afcf5 Binary files /dev/null and "b/code/BasicFeature/Media/AVCodec/screenshots/\346\222\255\346\224\276_\351\200\211\346\213\251\346\222\255\346\224\276\350\267\257\345\276\204.jpeg" differ