上一篇文章我们详细地讨论了图片相关的操作,最近有点忙时隔多日没有更新了,这一次我们继续视频图片操作一条龙系列--一起来捋一捋视频相关的操作--主要包含视频转码和视频缩略图的获取。
视频播放—AVPlayer和Video的区别
developer.huawei.com/consumer/cn…
正如官方文档所提到的:
- AVPlayer:功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件。
- Video组件:封装了视频播放的基础能力,需要设置数据源以及基础信息即可播放视频,但相对扩展能力较弱。Video组件由ArkUI提供能力,相关指导请参考UI开发文档-Video组件。
简单概述就是Video能用但是不如AVPlayer扩展性好,灵活性高,不过AVPlayer好像没有设置缩略图的属性呀,这点Video更胜一筹。
视频转码
developer.huawei.com/consumer/cn… 我们先来了解下相关概念:
码率
-
码率:指的是单位时间内视频流的数据量(单位:Kbps 或 Mbps)。1 Mbps = 1,000,000 bit/s(1,000,000比特每秒)。
码率越高,单位时间内传输的数据越多,潜在画质更高,但文件体积也更大。
分辨率
-
分辨率:指视频画面的像素数量(例如 1920×1080—宽×高)。
分辨率越高,像素数量越多,画面细节更加清晰,但需要处理的数据量也更大。
关系与转换公式
-
直观关系
在相同编码效率和内容复杂度的情况下,分辨率越大,则需要分配越高的码率以保持画质。如果所分配的码率不足,编码器会通过压缩(如丢弃细节、增加块效应)来降低数据量。
-
公式参考(经验法则)
- 码率正比于 分辨率宽×分辨率高×帧率×复杂度系数
- 复杂度系数与视频内容的动态程度相关,例如静态画面(例如讲座视频)低复杂度系数,可以较低码率保持清晰,动态画面(例如体育比赛)高复杂度系数,需要更高的码率。
-
码率转换
输入:源视频的宽wref、高href、帧率fpsref、码率Rref;目标视频的宽wtar、高htar、帧率fpstar。
输出:目标视频的码率Rtar。
计算过程:
分辨率和帧率的系数由以下经验公式计算可得。
上述计算帧率的公式y=clip(0.5, 2, x)表示:如果x∈[0.5, 2.0],取y=x;如果x<0.5,取y=0.5;如果x>2.0,取y=2.0。
-
码率计算
选定一个baseline的码率,例如720P/30fps的视频,码率默认3Mbps,记为V0。
如果要对视频V1做转码,输出视频为V2,可以按如下过程计算:
-
带入(V0,V2),得到估计码率为R2。
-
带入(V1,V2),得到估计码率为R2'。
取二者最小值,以确保目标码率比源视频有所降低。
-
-
分辨率设置参考(以H.264为例)
分辨率 动态内容(如游戏) 中等动态(如电影) 静态内容(如幻灯片) 720p(1280 × 720) 3.5–5 Mbps 2.5–4 Mbps 1–2 Mbps 1080p(1920 × 1080) 6–8 Mbps 4–6 Mbps 2–3 Mbps 4K(3840 × 2160) 25–35 Mbps 15–25 Mbps 10–15 Mbps -
转换样例
场景一:假设要转码一个分辨率1280×720,30fps的视频,码率为1Mbps,这是画质相对比较良好的视频。需要将视频转码为分辨率640×480,30fps的视频,码率应该设置为463,463bps。计算如下:
Resolution_factor = 0.463463
fps_factor = 1
Rtar = 463,463bps
场景二:假设要转码一个分辨率1280×720,30fps的视频,码率为1Mbps的视频。需要将视频转码为码率为600,000bps,30fps的视频,分辨率应该设置为888×500。计算如下:
fps_factor = 1
Rtar = 600,000bps
Resolution_factor = 0.482029
现在需要获取视频元数据根据经验公式动态计算转码配置
获取宽高,码率,帧率等信息:developer.huawei.com/consumer/cn…
developer.huawei.com/consumer/cn…
不使用media.AVMetadataExtractor是因为其只能获取到宽高等信息但不含有码率与帧率:
developer.huawei.com/consumer/cn…
代码示例:
获取视频元数据并计算转码配置
export interface VideoTrackInfo { // ④ 显式声明接口,替代 Object literal
width: number;
height: number;
bitrate: number;
frameRate: number;
}
/* 可调整的“输出模板” */
const TARGET_WIDTH = 1280;
const TARGET_HEIGHT = 720;
const TARGET_FPS = 30;
/* H.264 中等动态内容 baseline(截图表格) */
const BASELINE_MBPS: Record<string, number> = {
'720p': 3, // 2.5-4 Mbps 取中值
'1080p': 5, // 4-6 Mbps
'4K': 20, // 15-25 Mbps
};
/* 分辨率档次判断 */
function getResolutionLevel(w: number, h: number): string {
if (w >= 3840 || h >= 2160) {
return '4K';
}
if (w >= 1920 || h >= 1080) {
return '1080p';
}
return '720p';
}
/* clip 工具 */
function clip(min: number, max: number, v: number): number {
return Math.max(min, Math.min(max, v));
}
/**
* 传入本地文件路径,返回视频主轨信息
*/
async getVideoTrack(uri: string): Promise<VideoTrackInfo> {
console.info('获取视频详细信息');
let srcFd = -1;
let player: media.AVPlayer | null = null;
try {
/* 1. 打开文件 */
const srcFile = uri.includes('://')
? fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY)
: fs.openSync(uri, fs.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
/* 2. 创建播放器 */
player = await media.createAVPlayer();
player.fdSrc = { fd: srcFd };
/* 3. 等「initialized」→ 再 prepare → 再等「prepared」 */
await new Promise<void>((res, rej) => {
player!.on('stateChange', (s: string) => {
if (s === 'initialized') {
player!.prepare(); // 只有 initialized 才能调
}
if (s === 'prepared') {
res(); // prepared 完成才能往下走
}
if (s === 'error') {
rej(new Error('prepare fail'));
}
});
});
/* 4. 现在状态 = prepared,可以安全读轨道 */
const tracks = await player.getTrackDescription();
for (const t of tracks) {
if (t[media.MediaDescriptionKey.MD_KEY_TRACK_TYPE] === media.MediaType.MEDIA_TYPE_VID) {
const width = t[media.MediaDescriptionKey.MD_KEY_WIDTH] as number;
const height = t[media.MediaDescriptionKey.MD_KEY_HEIGHT] as number;
const bitrate = t[media.MediaDescriptionKey.MD_KEY_BITRATE] as number;
const frameRate = t[media.MediaDescriptionKey.MD_KEY_FRAME_RATE] as number;
console.info(`视频轨 ${width}×${height} 码率=${bitrate} bps 帧率=${frameRate / 100} fps`);
return {
width,
height,
bitrate,
frameRate: frameRate / 100
};
}
}
throw new Error('无视频轨');
} finally {
if (player) {
await player.release();
}
if (srcFd !== -1) {
fileIo.closeSync(srcFd);
}
}
}
async setConfig(uri: string): Promise<media.AVTranscoderConfig> {
const src = await this.getVideoTrack(uri); // 宽高、码率、帧率
/* 1. 系数计算(截图公式) */
const fpsFactor = clip(0.5, 2.0, (TARGET_FPS + src.frameRate) / (2 * src.frameRate));
const resolutionFactor = (TARGET_WIDTH * TARGET_HEIGHT) / (src.width * src.height);
const estimatedBitrate = fpsFactor * resolutionFactor * src.bitrate; // bps
/* 2. baseline 上限 */
const level = getResolutionLevel(TARGET_WIDTH, TARGET_HEIGHT);
const baselineBitrate = BASELINE_MBPS[level] * 1_000_000; // bps
/* 3. 取二者较小值,确保比源视频低 */
const targetBitrate = Math.min(estimatedBitrate, baselineBitrate, src.bitrate);
/* 4. 帧率同理不超过源视频 */
const targetFrameRate = Math.min(TARGET_FPS, src.frameRate);
console.info(`转码目标 ${TARGET_WIDTH}×${TARGET_HEIGHT} @${targetFrameRate} fps`);
console.info(`目标码率 ${(targetBitrate / 1_000_000).toFixed(2)} Mbps`);
/* 5. 返回 AVTranscoderConfig */
return {
fileFormat: media.ContainerFormatType.CFT_MPEG_4, // mp4
videoCodec: media.CodecMimeType.VIDEO_AVC, // H.264
audioCodec: media.CodecMimeType.AUDIO_AAC, // AAC
videoFrameWidth: TARGET_WIDTH,
videoFrameHeight: TARGET_HEIGHT,
videoBitrate: targetBitrate,
audioBitrate: 100000, // 128 kbps 默认值,按需改
};
}
转码:
// 开始转码对应的流程,返回转码后文件路径
async startTranscoderingProcess(uri: string): Promise<string> {
const cfg = await this.setConfig(uri);
// 下面再开始你的 AVTranscoder 流程
if (!canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
return Promise.reject('AVTranscoder unavailable');
}
// 1. 释放旧实例
if (this.avTranscoder) {
await this.avTranscoder.release();
}
// 2. 创建新实例
this.avTranscoder = await media.createAVTranscoder();
this.setAVTranscoderCallback();
let srcFd: number | null = null;
let fileSizeMB: number | null = null;
if (uri.includes('://')) {
const srcFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
const stat = fs.statSync(srcFd);
fileSizeMB = stat.size / (1024 * 1024);
} else {
const srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
const stat = fs.statSync(srcFd);
fileSizeMB = stat.size / (1024 * 1024);
}
console.log(`转码前文件的大小为${fileSizeMB}MB`)
// 3. 输入 fd
// const fdSrc = await this.context?.resourceManager.getRawFd(uri);
// this.avTranscoder.fdSrc = fdSrc!;
const fd: media.AVFileDescriptor = { fd: srcFd!, offset: 0, length: -1 }
this.avTranscoder.fdSrc = fd; //包装成AVFileDescriptor
// 4. 输出文件路径 & fd
const fileName = uri.split('/').pop() || `compressed_${Date.now()}.mp4`;
const compressedName = `compressed_${Date.now()}_${fileName.replace(/\.[^.]+$/, '.mp4')}`;
const outputFilePath = `${this.context?.cacheDir}/${compressedName}`;
const file = fs.openSync(outputFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
this.avTranscoder.fdDst = file.fd;
// 5. 准备 & 启动
await this.avTranscoder.prepare(cfg);
this.currentProgress = 0;
await this.avTranscoder.start();
const stat = fs.statSync(file.fd);
fileSizeMB = stat.size / (1024 * 1024);
console.log(`转码后文件的大小为${fileSizeMB}MB`)
// 6. 返回路径,供调用方上传
return outputFilePath;
}
对于异步转码,大家可以参考官方文档:
developer.huawei.com/consumer/cn…
主要增加了worker线程的创建,销毁以及worker进程与宿主进程之间的通信
(对于并发的实现,除了worker还可以使用TaskPool,感兴趣的同学可以看看官方文档的介绍和使用指南:developer.huawei.com/consumer/cn…
获取视频缩略图
developer.huawei.com/consumer/cn…
async getCover(fd: media.AVFileDescriptor): Promise<image.PixelMap>{
//获取视频缩略图
let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator();
avImageGenerator.fdSrc = fd
// pixelMap对象声明,用于图片显示。
let pixelMap: image.PixelMap | undefined = undefined;
// 初始化入参。
let timeUs = 0; // 需要获取的缩略图在视频中的时间点。
let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC; // AV_IMAGE_QUERY_NEXT_SYNC表示选取传入时间点或之后的关键帧。
// 输出缩略图的格式参数。
let param: media.PixelMapParams = {
width: 300, // 输出的缩略图宽度。
height: 300 // 输出的缩略图高度。
};
// 获取缩略图(promise模式)。
let vPlayer = await media.createAVPlayer();
pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
return pixelMap
}
//将缩略图写入沙箱中的临时文件--便于上传到后端或者存储桶
//将缩略图写进临时文件便于获取路径上传到阿里云OSS(考虑到兼容性,不使用buffer直接使用沙箱中的临时文件路径)
async copyCoverToCache(pixelMap: image.PixelMap, context: common.UIAbilityContext) {
/* 编码并写入沙箱临时文件 */
const packer = image.createImagePacker();
const buffer = await packer.packing(pixelMap, { format: 'image/jpeg', quality: 90 });
const tmpPath = context.cacheDir + '/context_' + Date.now() + '.jpg';
const fd = fileIo.openSync(tmpPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
fs.writeSync(fd.fd, buffer);
fs.closeSync(fd);
}
VIdeo组件可以使用previewUri属性设置缩略图
Video({
src: media.uri,
previewUri: media.pixelMap
})
注意:获取缩略图和转码不可以使用同一个fd实例—HarmonyOS 的音视频框架对同一个 fd 是「独占式」的
不同AVMetadataExtractor或者AVImageGenerator实例,如果需要操作同一资源,需要多次打开文件描述符,不要共用同一文件描述符。(media.AVFileDescriptor) 即不要这样写:
interface VideoInfo{
sandBoxPath: string;
cover:image.PixelMap
}
async startTranscoderingProcess(uri: string): Promise<VideoInfo> {
if (!canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
return Promise.reject('AVTranscoder unavailable');
}
// 1. 释放旧实例
if (this.avTranscoder) {
await this.avTranscoder.release();
}
// 2. 创建新实例
this.avTranscoder = await media.createAVTranscoder();
this.setAVTranscoderCallback();
let srcFd: number | null = null;
let fileSizeMB: number | null = null;
if (uri.includes('://')) {
const srcFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
const stat = fs.statSync(srcFd);
fileSizeMB = stat.size / (1024 * 1024);
} else {
const srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
const stat = fs.statSync(srcFd);
fileSizeMB = stat.size / (1024 * 1024);
}
console.log(`转码前文件的大小为${fileSizeMB}MB`)
// 3. 输入 fd
// const fdSrc = await this.context?.resourceManager.getRawFd(uri);
// this.avTranscoder.fdSrc = fdSrc!;
const fd: media.AVFileDescriptor = { fd: srcFd!, offset: 0, length: -1 }
this.avTranscoder.fdSrc = fd; //包装成AVFileDescriptor
// 4. 输出文件路径 & fd
const fileName = uri.split('/').pop() || `compressed_${Date.now()}.mp4`;
const compressedName = `compressed_${Date.now()}_${fileName.replace(/\.[^.]+$/, '.mp4')}`;
const outputFilePath = `${this.context?.cacheDir}/${compressedName}`;
const file = fs.openSync(outputFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
this.avTranscoder.fdDst = file.fd;
// 5. 准备 & 启动
await this.avTranscoder.prepare(this.avConfig);
this.currentProgress = 0;
await this.avTranscoder.start();
const stat = fs.statSync(file.fd);
fileSizeMB = stat.size / (1024 * 1024);
console.log(`转码后文件的大小为${fileSizeMB}MB`)
//获取缩略图
const coverImg:iamge.PixelMap = this.getCover(fd)
// 6. 返回路径,供调用方上传
return {sandBoxPath:outputFilePath,cover:coverImg};
}
async getCover(fd:media.AVFileDescriptor ): Promise<image.PixelMap> {
//获取视频缩略图
let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator();
avImageGenerator.fdSrc = fd
// pixelMap对象声明,用于图片显示。
let pixelMap: image.PixelMap | undefined = undefined;
// 初始化入参。
let timeUs = 0; // 需要获取的缩略图在视频中的时间点。
let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC; // AV_IMAGE_QUERY_NEXT_SYNC表示选取传入时间点或之后的关键帧。
// 输出缩略图的格式参数。
let param: media.PixelMapParams = {
width: 300, // 输出的缩略图宽度。
height: 300 // 输出的缩略图高度。
};
pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
// 释放资源(promise模式)。
avImageGenerator.release();
return pixelMap
}
完整工具类封装:
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
import fs, { ReadOptions } from '@ohos.file.fs';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import fileIo from '@ohos.file.fs';
import { image } from '@kit.ImageKit';
import fileIO from '@ohos.fileio';
export interface VideoTrackInfo { // ④ 显式声明接口,替代 Object literal
width: number;
height: number;
bitrate: number;
frameRate: number;
}
/* 可调整的“输出模板” */
const TARGET_WIDTH = 1280;
const TARGET_HEIGHT = 720;
const TARGET_FPS = 30;
/* H.264 中等动态内容 baseline(截图表格) */
const BASELINE_MBPS: Record<string, number> = {
'720p': 3, // 2.5-4 Mbps 取中值
'1080p': 5, // 4-6 Mbps
'4K': 20, // 15-25 Mbps
};
/* 分辨率档次判断 */
function getResolutionLevel(w: number, h: number): string {
if (w >= 3840 || h >= 2160) {
return '4K';
}
if (w >= 1920 || h >= 1080) {
return '1080p';
}
return '720p';
}
/* clip 工具 */
function clip(min: number, max: number, v: number): number {
return Math.max(min, Math.min(max, v));
}
export class AVTranscoderDemo {
private avTranscoder: media.AVTranscoder | undefined = undefined;
private context: Context | undefined;
private currentProgress: number = 0;
constructor(context: Context | undefined) {
if (context != undefined) {
this.context = context;
}
}
// 注册avTranscoder回调函数。
setAVTranscoderCallback() {
if (canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
if (this.avTranscoder != undefined) {
// 转码完成回调函数。
this.avTranscoder.on('complete', async () => {
console.log(`AVTranscoder is completed`);
await this.releaseTranscoderingProcess();
});
// 错误上报回调函数。
this.avTranscoder.on('error', (err: BusinessError) => {
console.error(`AVTranscoder failed, code is ${err.code}, message is ${err.message}`);
});
// 进度上报回调函数
this.avTranscoder.on('progressUpdate', (progress: number) => {
console.info(`AVTranscoder progressUpdate = ${progress}`);
this.currentProgress = progress;
})
}
}
}
// 开始转码对应的流程,返回转码后文件路径
async startTranscoderingProcess(uri: string): Promise<string> {
const cfg = await this.setConfig(uri);
// 下面再开始你的 AVTranscoder 流程
if (!canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
return Promise.reject('AVTranscoder unavailable');
}
// 1. 释放旧实例
if (this.avTranscoder) {
await this.avTranscoder.release();
}
// 2. 创建新实例
this.avTranscoder = await media.createAVTranscoder();
this.setAVTranscoderCallback();
let srcFd: number | null = null;
let fileSizeMB: number | null = null;
if (uri.includes('://')) {
const srcFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
const stat = fs.statSync(srcFd);
fileSizeMB = stat.size / (1024 * 1024);
} else {
const srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
const stat = fs.statSync(srcFd);
fileSizeMB = stat.size / (1024 * 1024);
}
console.log(`转码前文件的大小为${fileSizeMB}MB`)
// 3. 输入 fd
// const fdSrc = await this.context?.resourceManager.getRawFd(uri);
// this.avTranscoder.fdSrc = fdSrc!;
const fd: media.AVFileDescriptor = { fd: srcFd!, offset: 0, length: -1 }
this.avTranscoder.fdSrc = fd; //包装成AVFileDescriptor
// 4. 输出文件路径 & fd
const fileName = uri.split('/').pop() || `compressed_${Date.now()}.mp4`;
const compressedName = `compressed_${Date.now()}_${fileName.replace(/\.[^.]+$/, '.mp4')}`;
const outputFilePath = `${this.context?.cacheDir}/${compressedName}`;
const file = fs.openSync(outputFilePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
this.avTranscoder.fdDst = file.fd;
// 5. 准备 & 启动
await this.avTranscoder.prepare(cfg);
this.currentProgress = 0;
await this.avTranscoder.start();
const stat = fs.statSync(file.fd);
fileSizeMB = stat.size / (1024 * 1024);
console.log(`转码后文件的大小为${fileSizeMB}MB`)
// 6. 返回路径,供调用方上传
return outputFilePath;
}
/**
* 传入本地文件路径,返回视频主轨信息
*/
async getVideoTrack(uri: string): Promise<VideoTrackInfo> {
console.info('获取视频详细信息');
let srcFd = -1;
let player: media.AVPlayer | null = null;
try {
/* 1. 打开文件 */
const srcFile = uri.includes('://')
? fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY)
: fs.openSync(uri, fs.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
/* 2. 创建播放器 */
player = await media.createAVPlayer();
player.fdSrc = { fd: srcFd };
/* 3. 等「initialized」→ 再 prepare → 再等「prepared」 */
await new Promise<void>((res, rej) => {
player!.on('stateChange', (s: string) => {
if (s === 'initialized') {
player!.prepare(); // 只有 initialized 才能调
}
if (s === 'prepared') {
res(); // prepared 完成才能往下走
}
if (s === 'error') {
rej(new Error('prepare fail'));
}
});
});
/* 4. 现在状态 = prepared,可以安全读轨道 */
const tracks = await player.getTrackDescription();
for (const t of tracks) {
if (t[media.MediaDescriptionKey.MD_KEY_TRACK_TYPE] === media.MediaType.MEDIA_TYPE_VID) {
const width = t[media.MediaDescriptionKey.MD_KEY_WIDTH] as number;
const height = t[media.MediaDescriptionKey.MD_KEY_HEIGHT] as number;
const bitrate = t[media.MediaDescriptionKey.MD_KEY_BITRATE] as number;
const frameRate = t[media.MediaDescriptionKey.MD_KEY_FRAME_RATE] as number;
console.info(`视频轨 ${width}×${height} 码率=${bitrate} bps 帧率=${frameRate / 100} fps`);
return {
width,
height,
bitrate,
frameRate: frameRate / 100
};
}
}
throw new Error('无视频轨');
} finally {
if (player) {
await player.release();
}
if (srcFd !== -1) {
fileIo.closeSync(srcFd);
}
}
}
async setConfig(uri: string): Promise<media.AVTranscoderConfig> {
const src = await this.getVideoTrack(uri); // 宽高、码率、帧率
/* 1. 系数计算(截图公式) */
const fpsFactor = clip(0.5, 2.0, (TARGET_FPS + src.frameRate) / (2 * src.frameRate));
const resolutionFactor = (TARGET_WIDTH * TARGET_HEIGHT) / (src.width * src.height);
const estimatedBitrate = fpsFactor * resolutionFactor * src.bitrate; // bps
/* 2. baseline 上限 */
const level = getResolutionLevel(TARGET_WIDTH, TARGET_HEIGHT);
const baselineBitrate = BASELINE_MBPS[level] * 1_000_000; // bps
/* 3. 取二者较小值,确保比源视频低 */
const targetBitrate = Math.min(estimatedBitrate, baselineBitrate, src.bitrate);
/* 4. 帧率同理不超过源视频 */
const targetFrameRate = Math.min(TARGET_FPS, src.frameRate);
console.info(`转码目标 ${TARGET_WIDTH}×${TARGET_HEIGHT} @${targetFrameRate} fps`);
console.info(`目标码率 ${(targetBitrate / 1_000_000).toFixed(2)} Mbps`);
/* 5. 返回 AVTranscoderConfig */
return {
fileFormat: media.ContainerFormatType.CFT_MPEG_4, // mp4
videoCodec: media.CodecMimeType.VIDEO_AVC, // H.264
audioCodec: media.CodecMimeType.AUDIO_AAC, // AAC
videoFrameWidth: TARGET_WIDTH,
videoFrameHeight: TARGET_HEIGHT,
videoBitrate: targetBitrate,
audioBitrate: 100000, // 128 kbps 默认值,按需改
};
}
async getCover(uri: string): Promise<image.PixelMap> {
//获取文件描述符
let srcFd: number | null = null;
if (uri.includes('://')) {
const srcFile = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
} else {
const srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
srcFd = srcFile.fd;
}
const fd: media.AVFileDescriptor = { fd: srcFd!, offset: 0, length: -1 }
//获取视频缩略图
let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator();
avImageGenerator.fdSrc = fd
// pixelMap对象声明,用于图片显示。
let pixelMap: image.PixelMap | undefined = undefined;
// 初始化入参。
let timeUs = 0; // 需要获取的缩略图在视频中的时间点。
let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC; // AV_IMAGE_QUERY_NEXT_SYNC表示选取传入时间点或之后的关键帧。
// 输出缩略图的格式参数。
let param: media.PixelMapParams = {
width: 300, // 输出的缩略图宽度。
height: 300 // 输出的缩略图高度。
};
pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param);
// 释放资源(promise模式)。
avImageGenerator.release();
return pixelMap
}
// 暂停转码对应的流程。
async pauseTranscoderingProcess() {
if (canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
if (this.avTranscoder != undefined) { // 仅在调用start返回后调用pause为合理调用。
await this.avTranscoder.pause();
}
}
}
// 恢复对应的转码流程。
async resumeTranscoderingProcess() {
if (canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
if (this.avTranscoder != undefined) { // 仅在调用pause返回后调用resume为合理调用。
await this.avTranscoder.resume();
}
}
}
// 释放转码流程。
async releaseTranscoderingProcess() {
if (canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
if (this.avTranscoder != undefined) {
// 1.释放转码实例。
await this.avTranscoder.release();
this.avTranscoder = undefined;
// 2.关闭转码目标文件fd。
fs.closeSync(this.avTranscoder!.fdDst);
}
}
}
// 获取当前进度
getCurrentProgress(): number {
console.info(`getCurrentProgress = ${this.currentProgress}`);
return this.currentProgress;
}
}
这一集就到这里,如果有任何疑问或者有不足之处需要纠正,欢迎大家畅所欲言。下一个最终篇我们聊聊图片、视频文件的上传那些事。