【在鸿蒙系统中实现录制视频预览功能】

在鸿蒙系统中实现录制视频预览功能

一、配置权限和设备能力

c 复制代码
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.CAMERA"
      },
      {
        "name": "ohos.permission.MICROPHONE"
      },
      {
        "name": "ohos.permission.WRITE_MEDIA"
      },
      {
        "name": "ohos.permission.READ_MEDIA"
      }
    ],
    "abilities": [
      {
        "permissions": [
          "ohos.permission.CAMERA",
          "ohos.permission.MICROPHONE"
        ]
      }
    ]
  }
}

二、视频录制核心实现

c 复制代码
// VideoRecorder.ets
import camera from '@ohos.multimedia.camera';
import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';

@Entry
@Component
struct VideoRecorderPage {
  // 状态变量
  @State previewSurfaceId: string = '';
  @State isRecording: boolean = false;
  @State isPreviewing: boolean = false;
  @State recordingTime: string = '00:00';
  @State videoPath: string = '';
  @State flashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_OFF;
  
  // 相机相关
  private cameraManager: camera.CameraManager | null = null;
  private cameraInput: camera.CameraInput | null = null;
  private previewOutput: camera.PreviewOutput | null = null;
  private videoOutput: camera.VideoOutput | null = null;
  private cameraObj: camera.CameraDevice | null = null;
  
  // 录制相关
  private recorder: media.MediaRecorder | null = null;
  private timer: number = 0;
  private elapsedSeconds: number = 0;
  
  aboutToAppear() {
    this.initCamera();
  }
  
  aboutToDisappear() {
    this.releaseResources();
  }
  
  // 初始化相机
  async initCamera() {
    try {
      // 获取相机管理器
      this.cameraManager = await camera.getCameraManager(globalThis.abilityContext as common.Context);
      
      // 获取相机列表
      const cameras = this.cameraManager.getSupportedCameras();
      if (cameras.length === 0) {
        console.error('No camera found');
        return;
      }
      
      // 选择后置摄像头
      const backCamera = cameras.find(camera => camera.position === camera.CameraPosition.CAMERA_POSITION_BACK) || cameras[0];
      
      // 创建相机输入
      this.cameraInput = this.cameraManager.createCameraInput(backCamera);
      await this.cameraInput.open();
      
      // 创建相机会话
      await this.createCameraSession();
      
      // 开始预览
      await this.startPreview();
      
    } catch (error) {
      console.error('Camera initialization failed:', error);
    }
  }
  
  // 创建相机会话
  async createCameraSession() {
    if (!this.cameraManager || !this.cameraInput) {
      throw new Error('Camera not initialized');
    }
    
    // 获取相机输出能力
    const cameraOutputCap = this.cameraManager.getSupportedOutputCapability(
      this.cameraInput.cameraDevice,
      camera.SceneMode.NORMAL_VIDEO
    );
    
    // 创建预览输出
    const previewProfiles = cameraOutputCap.previewProfiles;
    if (previewProfiles.length === 0) {
      throw new Error('No preview profile available');
    }
    this.previewOutput = this.cameraManager.createPreviewOutput(
      previewProfiles[0],
      this.previewSurfaceId
    );
    
    // 创建视频输出
    const videoProfiles = cameraOutputCap.videoProfiles;
    if (videoProfiles.length > 0) {
      this.videoOutput = this.cameraManager.createVideoOutput(videoProfiles[0]);
    }
    
    // 创建相机会话
    const session = this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO);
    
    // 配置会话
    session.beginConfig();
    session.addInput(this.cameraInput);
    session.addOutput(this.previewOutput);
    if (this.videoOutput) {
      session.addOutput(this.videoOutput);
    }
    await session.commitConfig();
    
    this.cameraObj = this.cameraInput.cameraDevice;
    return session;
  }
  
  // 开始预览
  async startPreview() {
    if (!this.cameraObj) {
      return;
    }
    
    const session = this.cameraObj.session;
    await session.start();
    this.isPreviewing = true;
    console.log('Camera preview started');
  }
  
  // 开始录制视频
  async startRecording() {
    if (!this.videoOutput || this.isRecording) {
      return;
    }
    
    try {
      // 准备录制参数
      const context = globalThis.abilityContext as common.Context;
      const filesDir = context.filesDir;
      const timestamp = new Date().getTime();
      this.videoPath = `${filesDir}/video_${timestamp}.mp4`;
      
      // 创建录制器
      this.recorder = await media.createMediaRecorder();
      
      // 配置录制参数
      const recorderConfig: media.MediaRecorderOptions = {
        videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
        audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
        profile: {
          audioBitrate: 128000,
          audioChannels: 2,
          audioCodec: media.CodecMimeType.AUDIO_AAC,
          audioSampleRate: 48000,
          fileFormat: media.ContainerFormatType.CFT_MPEG_4,
          videoBitrate: 2000000,
          videoCodec: media.CodecMimeType.VIDEO_AVC,
          videoFrameWidth: 1920,
          videoFrameHeight: 1080,
          videoFrameRate: 30
        },
        url: this.videoPath,
        rotation: 0,
        location: { latitude: 0, longitude: 0 }
      };
      
      // 准备录制器
      await this.recorder.prepare(recorderConfig);
      
      // 获取视频表面并绑定到视频输出
      const surfaceId = await this.recorder.getInputSurface();
      this.videoOutput?.start().then(() => {
        this.videoOutput?.bindSurface(surfaceId).then(() => {
          // 开始录制
          this.recorder?.start();
          this.isRecording = true;
          this.startTimer();
          console.log('Recording started:', this.videoPath);
        });
      });
      
    } catch (error) {
      console.error('Start recording failed:', error);
    }
  }
  
  // 停止录制
  async stopRecording() {
    if (!this.recorder || !this.isRecording) {
      return;
    }
    
    try {
      // 停止录制
      await this.recorder.stop();
      
      // 释放录制器
      await this.recorder.release();
      this.recorder = null;
      
      // 解绑视频表面
      if (this.videoOutput) {
        await this.videoOutput.unbindSurface();
      }
      
      this.isRecording = false;
      this.stopTimer();
      console.log('Recording stopped. Video saved at:', this.videoPath);
      
    } catch (error) {
      console.error('Stop recording failed:', error);
    }
  }
  
  // 开始计时器
  startTimer() {
    this.elapsedSeconds = 0;
    this.recordingTime = '00:00';
    
    this.timer = setInterval(() => {
      this.elapsedSeconds++;
      const minutes = Math.floor(this.elapsedSeconds / 60);
      const seconds = this.elapsedSeconds % 60;
      this.recordingTime = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
    }, 1000) as unknown as number;
  }
  
  // 停止计时器
  stopTimer() {
    if (this.timer) {
      clearInterval(this.timer);
      this.timer = 0;
    }
    this.recordingTime = '00:00';
  }
  
  // 切换闪光灯
  async toggleFlash() {
    if (!this.cameraInput) return;
    
    try {
      const flashModes = [
        camera.FlashMode.FLASH_MODE_OFF,
        camera.FlashMode.FLASH_MODE_ON,
        camera.FlashMode.FLASH_MODE_AUTO,
        camera.FlashMode.FLASH_MODE_ALWAYS_OPEN
      ];
      
      const currentIndex = flashModes.indexOf(this.flashMode);
      const nextIndex = (currentIndex + 1) % flashModes.length;
      this.flashMode = flashModes[nextIndex];
      
      await this.cameraInput.setFlashMode(this.flashMode);
      console.log('Flash mode changed to:', this.flashMode);
    } catch (error) {
      console.error('Toggle flash failed:', error);
    }
  }
  
  // 切换摄像头
  async switchCamera() {
    if (this.isRecording) {
      console.log('Cannot switch camera while recording');
      return;
    }
    
    try {
      await this.releaseResources();
      await this.initCamera();
    } catch (error) {
      console.error('Switch camera failed:', error);
    }
  }
  
  // 释放资源
  releaseResources() {
    // 停止录制
    if (this.isRecording) {
      this.stopRecording();
    }
    
    // 停止预览
    if (this.cameraObj) {
      const session = this.cameraObj.session;
      session.stop();
      session.release();
    }
    
    // 关闭相机输入
    if (this.cameraInput) {
      this.cameraInput.close();
    }
    
    // 停止计时器
    this.stopTimer();
    
    this.isPreviewing = false;
    this.isRecording = false;
  }
  
  build() {
    Column({ space: 0 }) {
      // 相机预览区域
      Stack() {
        // 相机预览
        XComponent({
          id: 'cameraPreview',
          type: 'surface',
          controller: new XComponentController()
        })
        .width('100%')
        .height('100%')
        .onLoad((xComponentContext?: XComponentContext) => {
          if (xComponentContext) {
            this.previewSurfaceId = xComponentContext.getXComponentSurfaceId();
            console.log('Surface ID:', this.previewSurfaceId);
          }
        })
        
        // 录制计时器
        if (this.isRecording) {
          Text(this.recordingTime)
            .fontSize(24)
            .fontColor(Color.White)
            .backgroundColor('#66000000')
            .padding({ left: 20, right: 20, top: 10, bottom: 10 })
            .borderRadius(20)
            .margin({ top: 50 })
        }
        
        // 录制指示灯
        if (this.isRecording) {
          Row() {
            Circle({ width: 12, height: 12 })
              .fill(Color.Red)
          }
          .margin({ top: 50, left: '50%' })
          .translate({ x: 50 })
        }
        
        // 控制按钮区域(底部)
        Column({ space: 20 }) {
          // 顶部控制栏
          Row() {
            // 闪光灯按钮
            Button(this.getFlashIcon())
              .width(50)
              .height(50)
              .backgroundColor('#33000000')
              .borderRadius(25)
              .onClick(() => {
                this.toggleFlash();
              })
            
            // 切换摄像头按钮
            Button('↻')
              .width(50)
              .height(50)
              .backgroundColor('#33000000')
              .borderRadius(25)
              .onClick(() => {
                this.switchCamera();
              })
          }
          .width('100%')
          .justifyContent(FlexAlign.SpaceBetween)
          .padding({ left: 20, right: 20 })
          
          // 中央录制按钮
          Row() {
            if (this.isRecording) {
              // 录制中按钮
              Button('')
                .width(80)
                .height(80)
                .backgroundColor(Color.Red)
                .borderRadius(40)
                .onClick(() => {
                  this.stopRecording();
                })
            } else {
              // 开始录制按钮
              Button('')
                .width(70)
                .height(70)
                .backgroundColor(Color.Red)
                .borderRadius(35)
                .border({ width: 4, color: Color.White })
                .onClick(() => {
                  this.startRecording();
                })
            }
          }
          .width('100%')
          .justifyContent(FlexAlign.Center)
          
          // 状态信息
          Text(this.isRecording ? '录制中...' : '准备录制')
            .fontSize(16)
            .fontColor(Color.White)
            .backgroundColor('#33000000')
            .padding(10)
            .borderRadius(5)
        }
        .width('100%')
        .position({ x: 0, y: '80%' })
      }
      .width('100%')
      .height('80%')
      .backgroundColor(Color.Black)
      
      // 视频列表区域
      if (this.videoPath) {
        List() {
          ListItem() {
            Row({ space: 15 }) {
              // 视频缩略图(实际应用中需要生成缩略图)
              Image($r('app.media.video_thumb'))
                .width(60)
                .height(60)
                .borderRadius(8)
              
              Column({ space: 5 }) {
                Text('最新录制')
                  .fontSize(16)
                  .fontColor(Color.Black)
                
                Text(this.videoPath)
                  .fontSize(12)
                  .fontColor(Color.Gray)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
              }
              .layoutWeight(1)
              
              Button('播放')
                .width(60)
                .height(30)
                .fontSize(12)
                .onClick(() => {
                  this.playVideo(this.videoPath);
                })
            }
            .width('100%')
            .padding(10)
          }
        }
        .width('100%')
        .height('20%')
        .divider({ strokeWidth: 1, color: '#F1F1F1' })
      }
    }
    .width('100%')
    .height('100%')
  }
  
  // 获取闪光灯图标
  getFlashIcon(): string {
    switch (this.flashMode) {
      case camera.FlashMode.FLASH_MODE_OFF:
        return '⚡️';
      case camera.FlashMode.FLASH_MODE_ON:
        return '⚡️';
      case camera.FlashMode.FLASH_MODE_AUTO:
        return '⚡️A';
      case camera.FlashMode.FLASH_MODE_ALWAYS_OPEN:
        return '⚡️●';
      default:
        return '⚡️';
    }
  }
  
  // 播放视频
  async playVideo(videoPath: string) {
    try {
      // 使用媒体播放器播放视频
      const context = globalThis.abilityContext as common.Context;
      
      // 这里可以跳转到视频播放页面,或者使用弹窗播放
      console.log('Play video:', videoPath);
      
      // 示例:使用系统能力打开视频
      // 实际应用中可能需要使用媒体播放器组件
      promptAction.showToast({
        message: `播放视频: ${videoPath}`,
        duration: 2000
      });
      
    } catch (error) {
      console.error('Play video failed:', error);
    }
  }
}

三、视频录制设置页面

c 复制代码
// VideoSettings.ets - 视频设置
@Component
struct VideoSettings {
  @State videoQuality: number = 2; // 0:低, 1:中, 2:高
  @State frameRate: number = 30;
  @State audioEnabled: boolean = true;
  
  build() {
    Column({ space: 20 }) {
      Text('视频设置')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
      
      // 视频质量选择
      Text('视频质量')
      Row({ space: 10 }) {
        ForEach([0, 1, 2], (quality) => {
          Button(this.getQualityText(quality))
            .backgroundColor(this.videoQuality === quality ? Color.Blue : Color.Gray)
            .fontColor(Color.White)
            .onClick(() => {
              this.videoQuality = quality;
            })
        })
      }
      
      // 帧率选择
      Text(`帧率: ${this.frameRate}fps`)
      Slider({
        value: this.frameRate,
        min: 15,
        max: 60,
        step: 1,
        style: SliderStyle.OutSet
      })
      .onChange((value: number) => {
        this.frameRate = value;
      })
      
      // 音频开关
      Toggle({ type: ToggleType.Checkbox, isOn: this.audioEnabled })
        .onChange((isOn: boolean) => {
          this.audioEnabled = isOn;
        })
      Text('启用音频')
    }
    .padding(20)
  }
  
  getQualityText(quality: number): string {
    switch (quality) {
      case 0: return '低质量';
      case 1: return '中等质量';
      case 2: return '高质量';
      default: return '未知';
    }
  }
}

四、使用示例

c 复制代码
@Entry
@Component
struct MainPage {
  @State showRecorder: boolean = false;
  @State showSettings: boolean = false;
  
  build() {
    Column() {
      if (this.showRecorder) {
        VideoRecorderPage()
      } else if (this.showSettings) {
        VideoSettings()
      } else {
        Column({ space: 30 }) {
          Text('视频录制应用')
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
          
          Button('开始录制视频')
            .width(200)
            .height(60)
            .fontSize(18)
            .onClick(() => {
              this.showRecorder = true;
            })
          
          Button('录制设置')
            .width(200)
            .height(60)
            .fontSize(18)
            .onClick(() => {
              this.showSettings = true;
            })
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
      }
    }
  }
}

五、注意事项

  1. 权限管理:确保在运行时申请必要的权限
  2. 资源释放:及时释放相机和媒体资源,避免内存泄漏
  3. 错误处理:添加全面的错误处理机制

性能优化:

  1. 合理设置视频分辨率和码率
  2. 及时释放不再使用的资源
  3. 避免频繁创建和销毁对象
  4. 存储管理:定期清理旧的视频文件
  5. 电量优化:录制视频时注意电量消耗
相关推荐
m0_685535083 小时前
Zemax 车载前视ADAS镜头
华为·光学·光学设计·光学工程·镜头设计
萌虎不虎6 小时前
【鸿蒙ETS中WebSocket使用说明】
websocket·华为·harmonyos
GISer_Jing7 小时前
AI在前端开发&营销领域应用
前端·aigc·音视频
子榆.7 小时前
Flutter 与开源鸿蒙(OpenHarmony)工程化实践:CI/CD、性能监控与多端发布
flutter·开源·harmonyos
夏小鱼的blog8 小时前
【HarmonyOS应用开发入门】第三期:ArkTS语言基础(一)
harmonyos
子榆.10 小时前
Flutter 与开源鸿蒙(OpenHarmony)国际化与无障碍适配指南:打造真正包容的跨平台应用
flutter·华为·开源·harmonyos
C雨后彩虹11 小时前
斗地主之顺子
java·数据结构·算法·华为·面试
音视频牛哥11 小时前
深入探讨后台摄像头|麦克风采集与轻量级RTSP服务|RTMP推流架构设计
音视频·大牛直播sdk·安卓camera2采集推送·安卓camera2后台采集推流·安卓camera2后台rtmp·安卓camera2后台rtsp·camera2后台rtsp服务
子榆.11 小时前
Flutter 与开源鸿蒙(OpenHarmony)深度集成:从原理到实战进阶
flutter·华为·开源·harmonyos