[鸿蒙从零到一] HarmonyOS 媒体能力实战:图片、音频与视频处理

鸿蒙从零到一 HarmonyOS 媒体能力实战:图片、音频与视频处理

前言

在移动应用开发中,媒体能力是核心功能之一。HarmonyOS 提供了完整的媒体框架,覆盖图片选择、音频播放、视频录制等场景。本文将从实战角度出发,带你掌握 HarmonyOS 的媒体能力。


一、图片选择与处理

1.1 使用 Picker 选择图片

HarmonyOS 提供了统一的 Picker API,支持从相册选择图片:

typescript 复制代码
import { picker } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

async function pickImage(): Promise<string> {
  try {
    const photoSelectOptions = new picker.PhotoSelectOptions();
    photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
    photoSelectOptions.maxSelectNumber = 1;
    
    const photoViewPicker = new picker.PhotoViewPicker();
    const result = await photoViewPicker.select(photoSelectOptions);
    
    if (result && result.photoUris.length > 0) {
      return result.photoUris[0];
    }
    return '';
  } catch (err) {
    console.error('选择图片失败:', JSON.stringify(err));
    return '';
  }
}

1.2 图片解码与显示

获取图片 URI 后,使用 Image 组件显示:

typescript 复制代码
import { image } from '@kit.ImageKit';

@Entry
@Component
struct ImageDemo {
  @State imageUri: string = '';

  build() {
    Column() {
      Button('选择图片')
        .onClick(async () => {
          this.imageUri = await pickImage();
        })
      
      if (this.imageUri) {
        Image(this.imageUri)
          .width('100%')
          .height(300)
          .objectFit(ImageFit.Contain)
      }
    }
    .padding(20)
  }
}

1.3 图片压缩与保存

处理大图时需要压缩:

typescript 复制代码
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';

async function compressImage(sourceUri: string, targetPath: string): Promise<void> {
  try {
    const imageSource = image.createImageSource(sourceUri);
    const imageInfo = await imageSource.getImageInfo();
    console.info(`原始尺寸: ${imageInfo.size.width}x${imageInfo.size.height}`);
    
    const decodingOptions: image.DecodingOptions = {
      desiredSize: { width: 800, height: 800 },
      desiredPixelFormat: image.PixelMapFormat.RGBA_8888
    };
    
    const pixelMap = await imageSource.createPixelMap(decodingOptions);
    const imagePacker = image.createImagePacker();
    const packOpts: image.PackingOption = {
      format: 'image/jpeg',
      quality: 80
    };
    
    const buffer = await imagePacker.packing(pixelMap, packOpts);
    const file = fileIo.openSync(targetPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
    fileIo.writeSync(file.fd, buffer);
    fileIo.closeSync(file);
    
    console.info('图片压缩完成');
  } catch (err) {
    console.error('压缩失败:', JSON.stringify(err));
  }
}

二、音频播放与录制

2.1 音频播放

使用 AVPlayer 播放音频:

typescript 复制代码
import { media } from '@kit.MediaKit';

@Component
export struct AudioPlayer {
  private avPlayer?: media.AVPlayer;
  @State isPlaying: boolean = false;
  @State currentTime: number = 0;
  @State duration: number = 0;

  async initPlayer(audioUri: string) {
    try {
      this.avPlayer = await media.createAVPlayer();
      
      this.avPlayer.on('stateChange', (state: string) => {
        console.info(`播放器状态: ${state}`);
      });
      
      this.avPlayer.on('timeUpdate', (time: number) => {
        this.currentTime = time;
      });
      
      this.avPlayer.on('durationUpdate', (duration: number) => {
        this.duration = duration;
      });
      
      this.avPlayer.url = audioUri;
    } catch (err) {
      console.error('初始化播放器失败:', JSON.stringify(err));
    }
  }

  async play() {
    await this.avPlayer?.play();
    this.isPlaying = true;
  }

  async pause() {
    await this.avPlayer?.pause();
    this.isPlaying = false;
  }

  build() {
    Column() {
      Text(`${this.formatTime(this.currentTime)} / ${this.formatTime(this.duration)}`)
      
      Row() {
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => {
            if (this.isPlaying) {
              this.pause();
            } else {
              this.play();
            }
          })
      }
    }
  }

  formatTime(ms: number): string {
    const seconds = Math.floor(ms / 1000);
    const min = Math.floor(seconds / 60);
    const sec = seconds % 60;
    return `${min}:${sec.toString().padStart(2, '0')}`;
  }

  aboutToDisappear() {
    this.avPlayer?.release();
  }
}

2.2 音频录制

使用 AVRecorder 录制音频:

typescript 复制代码
import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';

@Component
export struct AudioRecorder {
  private avRecorder?: media.AVRecorder;
  @State isRecording: boolean = false;
  private outputPath: string = '';

  async initRecorder() {
    try {
      this.avRecorder = await media.createAVRecorder();
      
      this.avRecorder.on('stateChange', (state: string) => {
        console.info(`录制器状态: ${state}`);
      });
      
      const context = getContext(this);
      this.outputPath = `${context.cacheDir}/audio_${Date.now()}.m4a`;
      
      const config: media.AVRecorderConfig = {
        audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
        profile: {
          audioBitrate: 128000,
          audioChannels: 2,
          audioCodec: media.CodecMimeType.AUDIO_AAC,
          audioSampleRate: 48000,
          fileFormat: media.ContainerFormatType.CFT_MPEG_4A
        },
        url: `fd://${fileIo.openSync(this.outputPath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY).fd}`
      };
      
      await this.avRecorder.prepare(config);
    } catch (err) {
      console.error('初始化录制器失败:', JSON.stringify(err));
    }
  }

  async startRecord() {
    await this.avRecorder?.start();
    this.isRecording = true;
  }

  async stopRecord() {
    await this.avRecorder?.stop();
    this.isRecording = false;
    console.info('录音已保存:', this.outputPath);
  }

  build() {
    Column() {
      Button(this.isRecording ? '停止录音' : '开始录音')
        .onClick(() => {
          if (this.isRecording) {
            this.stopRecord();
          } else {
            this.startRecord();
          }
        })
    }
  }

  aboutToDisappear() {
    this.avRecorder?.release();
  }
}

三、视频播放与录制

3.1 视频播放

使用 AVPlayer + XComponent 播放视频:

typescript 复制代码
import { media } from '@kit.MediaKit';

@Entry
@Component
struct VideoPlayer {
  private avPlayer?: media.AVPlayer;
  private surfaceId: string = '';
  @State isPlaying: boolean = false;

  async initPlayer(videoUri: string) {
    try {
      this.avPlayer = await media.createAVPlayer();
      
      this.avPlayer.on('stateChange', (state: string) => {
        console.info(`播放器状态: ${state}`);
      });
      
      this.avPlayer.url = videoUri;
      this.avPlayer.surfaceId = this.surfaceId;
    } catch (err) {
      console.error('初始化播放器失败:', JSON.stringify(err));
    }
  }

  build() {
    Column() {
      XComponent({
        id: 'video_surface',
        type: XComponentType.SURFACE,
        controller: new XComponentController()
      })
        .onLoad((context?: object) => {
          this.surfaceId = (context as { surfaceId: string }).surfaceId;
          this.initPlayer('file://...');
        })
        .width('100%')
        .height(300)
      
      Button(this.isPlaying ? '暂停' : '播放')
        .onClick(async () => {
          if (this.isPlaying) {
            await this.avPlayer?.pause();
          } else {
            await this.avPlayer?.play();
          }
          this.isPlaying = !this.isPlaying;
        })
    }
  }
}

3.2 视频录制

使用相机 API 录制视频:

typescript 复制代码
import { camera } from '@kit.CameraKit';

@Component
export struct VideoRecorder {
  private cameraManager?: camera.CameraManager;
  private videoOutput?: camera.VideoOutput;
  @State isRecording: boolean = false;

  async initCamera() {
    try {
      this.cameraManager = camera.getCameraManager(getContext(this));
      const cameras = this.cameraManager.getSupportedCameras();
      
      if (cameras.length === 0) {
        console.error('没有可用相机');
        return;
      }
      
      const cameraInput = this.cameraManager.createCameraInput(cameras[0]);
      await cameraInput.open();
      
      const profile: camera.VideoProfile = {
        format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP,
        size: { width: 1920, height: 1080 },
        frameRateRange: { min: 30, max: 30 }
      };
      
      this.videoOutput = this.cameraManager.createVideoOutput(profile, 'fd://...');
      
      const session = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO);
      session.beginConfig();
      session.addInput(cameraInput);
      session.addOutput(this.videoOutput);
      await session.commitConfig();
      await session.start();
      
      console.info('相机初始化完成');
    } catch (err) {
      console.error('初始化相机失败:', JSON.stringify(err));
    }
  }

  async startRecord() {
    await this.videoOutput?.start();
    this.isRecording = true;
  }

  async stopRecord() {
    await this.videoOutput?.stop();
    this.isRecording = false;
  }

  build() {
    Column() {
      Button(this.isRecording ? '停止录制' : '开始录制')
        .onClick(() => {
          if (this.isRecording) {
            this.stopRecord();
          } else {
            this.startRecord();
          }
        })
    }
  }
}

四、权限申请

媒体功能需要申请相应权限:

4.1 module.json5 配置

json5 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:permission_read_media",
        "usedScene": { "when": "inuse" }
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:permission_write_media",
        "usedScene": { "when": "inuse" }
      },
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:permission_microphone",
        "usedScene": { "when": "inuse" }
      },
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:permission_camera",
        "usedScene": { "when": "inuse" }
      }
    ]
  }
}

4.2 运行时申请

typescript 复制代码
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';

async function requestPermissions(): Promise<boolean> {
  const permissions: Permissions[] = [
    'ohos.permission.READ_IMAGEVIDEO',
    'ohos.permission.MICROPHONE',
    'ohos.permission.CAMERA'
  ];
  
  const context = getContext(this);
  const atManager = abilityAccessCtrl.createAtManager();
  
  try {
    const result = await atManager.requestPermissionsFromUser(context, permissions);
    return result.authResults.every(r => r === 0);
  } catch (err) {
    console.error('权限申请失败:', JSON.stringify(err));
    return false;
  }
}

五、实战案例:完整的媒体播放器

typescript 复制代码
import { media } from '@kit.MediaKit';
import { picker } from '@kit.CoreFileKit';

@Entry
@Component
struct MediaPlayerDemo {
  private avPlayer?: media.AVPlayer;
  @State mediaUri: string = '';
  @State isPlaying: boolean = false;
  @State currentTime: number = 0;
  @State duration: number = 0;

  async selectMedia() {
    try {
      const options = new picker.PhotoSelectOptions();
      options.MIMEType = picker.PhotoViewMIMETypes.VIDEO_TYPE;
      options.maxSelectNumber = 1;
      
      const photoPicker = new picker.PhotoViewPicker();
      const result = await photoPicker.select(options);
      
      if (result.photoUris.length > 0) {
        this.mediaUri = result.photoUris[0];
        await this.initPlayer();
      }
    } catch (err) {
      console.error('选择媒体失败:', JSON.stringify(err));
    }
  }

  async initPlayer() {
    this.avPlayer = await media.createAVPlayer();
    
    this.avPlayer.on('timeUpdate', (time: number) => {
      this.currentTime = time;
    });
    
    this.avPlayer.on('durationUpdate', (duration: number) => {
      this.duration = duration;
    });
    
    this.avPlayer.url = this.mediaUri;
  }

  build() {
    Column() {
      Text('HarmonyOS 媒体播放器')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
      
      Button('选择视频')
        .onClick(() => this.selectMedia())
        .margin({ top: 20 })
      
      if (this.mediaUri) {
        Text(`播放中: ${this.mediaUri.split('/').pop()}`)
          .margin({ top: 10 })
        
        Row() {
          Button(this.isPlaying ? '暂停' : '播放')
            .onClick(async () => {
              if (this.isPlaying) {
                await this.avPlayer?.pause();
              } else {
                await this.avPlayer?.play();
              }
              this.isPlaying = !this.isPlaying;
            })
          
          Button('停止')
            .onClick(async () => {
              await this.avPlayer?.stop();
              this.isPlaying = false;
            })
        }
        .margin({ top: 20 })
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }

  aboutToDisappear() {
    this.avPlayer?.release();
  }
}

六、性能优化建议

6.1 图片加载优化

  • 使用缩略图预览大图
  • 异步解码避免主线程阻塞
  • 实现图片缓存机制

6.2 音视频播放优化

  • 预加载下一个媒体文件
  • 实现播放进度保存与恢复
  • 监听系统中断事件(来电等)

6.3 内存管理

  • 及时释放 PixelMap 和 AVPlayer
  • 使用弱引用避免内存泄漏
  • 监控内存占用并主动回收

总结

本文系统介绍了 HarmonyOS 的媒体能力,涵盖:

  1. 图片处理 --- Picker 选择、解码显示、压缩保存
  2. 音频能力 --- AVPlayer 播放、AVRecorder 录制
  3. 视频能力 --- 视频播放、相机录制
  4. 权限管理 --- 静态声明与动态申请
  5. 实战案例 --- 完整媒体播放器实现
  6. 性能优化 --- 内存管理与加载优化

掌握这些能力后,你就可以开发功能完整的媒体类应用了。


本文基于 HarmonyOS NEXT(API 12+)编写,部分 API 可能随版本更新而变化。

相关推荐
打呵欠的猫1 小时前
我让 AI 封装了一个 ImageUpload 组件,它设计的 5 层校验链路比我想的周全
前端·ai编程
AlexMaybeBot1 小时前
躺在沙发上开发 Openclaw 的移动端APP
前端·flutter
用户2181697049301 小时前
Flutter(十三)Text Image TextField
前端
程序员黑豆1 小时前
深入解析Java数据类型:基本类型与引用类型的本质区别与实战选择
前端·ai编程·全栈
东方小月2 小时前
从零开发一个 Coding Agent(七):实现纯文本 Agent Loop
前端·人工智能
大家的林语冰2 小时前
👉 尤雨溪再次成立新公司,同时官宣 Pinia 4 正式发布!
前端·javascript·vue.js
浮生望2 小时前
React useRef 深度解析:从DOM操作到Web Worker多线程协作
前端
浮生望2 小时前
React Router v7 实战全景:路由配置、嵌套路由、鉴权守卫与懒加载完整指南
前端
用户938515635072 小时前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战(二)
前端·javascript·typescript