CHAPTER 06 · 10

多媒体与传感器

掌握相机拍照、音视频播放与录制、图片处理(PixelMap)以及传感器 API,开发功能完整的多媒体应用。

多媒体权限声明

所有多媒体功能都涉及用户隐私,必须在 module.json5 中声明对应权限,并在运行时向用户申请。HarmonyOS NEXT 的权限分为两类:

// module.json5
{
  "requestPermissions": [
    { "name": "ohos.permission.CAMERA" },
    { "name": "ohos.permission.MICROPHONE" },
    { "name": "ohos.permission.READ_MEDIA" },
    { "name": "ohos.permission.WRITE_MEDIA" },
    { "name": "ohos.permission.LOCATION" },
    {
      "name": "ohos.permission.LOCATION_IN_BACKGROUND",
      "reason": "$string:location_reason",  // 必须提供使用理由
      "usedScene": {
        "abilities": ["EntryAbility"],
        "when": "inuse"
      }
    }
  ]
}

运行时权限申请

import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
import bundleManager from '@ohos.bundle.bundleManager'

// 通用权限申请函数
async function requestPermission(
  context: Context,
  permissions: Permissions[]
): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager()

  // 先检查是否已授权
  const grantResults = await atManager.requestPermissionsFromUser(context, permissions)

  // 检查所有权限是否都已授权
  return grantResults.authResults.every(
    result => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
  )
}

// 使用示例:申请相机权限
async function openCamera(context: Context) {
  const granted = await requestPermission(context, [
    'ohos.permission.CAMERA',
    'ohos.permission.MICROPHONE'
  ])
  if (!granted) {
    promptAction.showToast({ message: '需要相机和麦克风权限' })
    return
  }
  // 权限获取成功,继续相机操作
}

相机开发

HarmonyOS NEXT 提供了 @ohos.multimedia.camera 模块用于相机操控,以及 XComponent 组件作为相机预览界面:

import camera from '@ohos.multimedia.camera'
import image from '@ohos.multimedia.image'

@Entry
@Component
struct CameraPage {
  private cameraManager?: camera.CameraManager
  private captureSession?: camera.PhotoSession
  private photoOutput?: camera.PhotoOutput
  @State capturedImage: string = ''

  aboutToAppear() {
    this.initCamera()
  }

  aboutToDisappear() {
    this.releaseCamera()  // 必须释放,否则相机资源泄漏
  }

  async initCamera() {
    // 获取 CameraManager
    this.cameraManager = camera.getCameraManager(getContext(this))

    // 获取可用摄像头列表(前后摄)
    const cameras = this.cameraManager.getSupportedCameras()
    const backCamera = cameras.find(
      c => c.cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK
    )
    if (!backCamera) return

    // 创建相机输入
    const cameraInput = this.cameraManager.createCameraInput(backCamera)
    await cameraInput.open()

    // 创建拍照输出
    const photoProfiles = this.cameraManager
      .getSupportedOutputCapability(backCamera, camera.SceneMode.NORMAL_PHOTO)
      .photoProfiles
    this.photoOutput = this.cameraManager.createPhotoOutput(photoProfiles[0])

    // 监听拍照完成事件
    this.photoOutput.on('photoAvailable', (photo: camera.Photo) => {
      this.handlePhoto(photo)
    })
  }

  async takePhoto() {
    if (!this.photoOutput) return
    const settings: camera.PhotoCaptureSetting = {
      quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
      rotation: camera.ImageRotation.ROTATION_0
    }
    await this.photoOutput.capture(settings)
  }

  async handlePhoto(photo: camera.Photo) {
    // 将相机原始数据转为 PixelMap
    const buffer = new ArrayBuffer(photo.main.capacity)
    await photo.main.readBuffer(buffer)

    const imageSource = image.createImageSource(buffer)
    const pixelMap = await imageSource.createPixelMap()

    // 保存到沙箱目录
    const path = `${getContext(this).filesDir}/photo_${Date.now()}.jpg`
    const packOpts: image.PackingOption = { format: 'image/jpeg', quality: 90 }
    const imagePacker = image.createImagePacker()
    const data = await imagePacker.packing(pixelMap, packOpts)
    // 写入文件...
    this.capturedImage = path
  }

  async releaseCamera() {
    await this.captureSession?.stop()
    await this.captureSession?.release()
  }

  build() {
    Stack({ alignContent: Alignment.Bottom }) {
      // XComponent 是相机预览的宿主(系统级别,性能最优)
      XComponent({
        id: 'cameraPreview',
        type: XComponentType.SURFACE,
        controller: new XComponentController()
      })
      .width('100%')
      .height('100%')

      // 底部拍照按钮
      Button('📷 拍照')
        .width(80)
        .height(80)
        .borderRadius(40)
        .backgroundColor('rgba(255,255,255,0.2)')
        .border({ width: 3, color: '#ffffff' })
        .margin({ bottom: 40 })
        .onClick(() => this.takePhoto())
    }
    .width('100%')
    .height('100%')
  }
}

音视频播放

HarmonyOS NEXT 使用 AVPlayer(Audio/Video Player)统一处理音频和视频播放:

import media from '@ohos.multimedia.media'

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

  aboutToAppear() {
    this.initPlayer()
  }

  async initPlayer() {
    // 创建 AVPlayer 实例
    this.avPlayer = await media.createAVPlayer()

    // 监听状态变化
    this.avPlayer.on('stateChange', (state: string) => {
      switch (state) {
        case 'prepared':
          this.duration = this.avPlayer!.duration
          console.log('播放器就绪,时长:', this.duration)
          break
        case 'playing':
          this.isPlaying = true
          break
        case 'paused':
        case 'stopped':
          this.isPlaying = false
          break
        case 'completed':
          this.isPlaying = false
          this.currentTime = 0
          break
      }
    })

    // 监听播放进度
    this.avPlayer.on('timeUpdate', (time: number) => {
      this.currentTime = time
    })

    // 设置视频源(支持网络 URL 或本地路径)
    this.avPlayer.url = 'https://example.com/video.mp4'
  }

  build() {
    Column({ space: 16 }) {
      // 视频渲染到 XComponent
      XComponent({
        id: 'videoSurface',
        type: XComponentType.SURFACE,
        controller: new XComponentController()
      })
      .width('100%')
      .aspectRatio(16 / 9)

      // 进度条
      Slider({
        value: this.currentTime,
        min: 0,
        max: this.duration,
        style: SliderStyle.OutSet
      })
      .trackColor('#21262d')
      .selectedColor('#CF0A2C')
      .onChange((value: number) => {
        this.avPlayer?.seek(value)  // 跳转到指定时间(ms)
      })

      // 播放控制
      Row({ space: 24 }) {
        Button('⏮').onClick(() => this.avPlayer?.seek(0))
        Button(this.isPlaying ? '⏸' : '▶')
          .onClick(() => {
            if (this.isPlaying) {
              this.avPlayer?.pause()
            } else {
              this.avPlayer?.play()
            }
          })
        Button('⏹').onClick(() => this.avPlayer?.stop())
      }
    }
    .padding(20)
  }
}

图片处理与 PixelMap

PixelMap 是 HarmonyOS NEXT 的图像数据容器,所有图片处理操作(缩放、裁剪、旋转、滤镜)都基于它。理解 PixelMap 是图像处理的基础:

import image from '@ohos.multimedia.image'

class ImageProcessor {
  // 从网络 URL 加载图片为 PixelMap
  static async loadFromUrl(url: string): Promise<image.PixelMap> {
    // 先下载图片字节数据
    const request = http.createHttp()
    const response = await request.request(url, {
      method: http.RequestMethod.GET,
      expectDataType: http.HttpDataType.ARRAY_BUFFER
    })
    request.destroy()

    // 用 ImageSource 解码为 PixelMap
    const source = image.createImageSource(response.result as ArrayBuffer)
    return await source.createPixelMap({
      desiredPixelFormat: image.PixelMapFormat.RGBA_8888
    })
  }

  // 裁剪 PixelMap
  static async crop(
    pixelMap: image.PixelMap,
    region: { x: number; y: number; width: number; height: number }
  ): Promise<image.PixelMap> {
    await pixelMap.crop(region)
    return pixelMap
  }

  // 缩放图片
  static async resize(
    pixelMap: image.PixelMap,
    targetWidth: number,
    targetHeight: number
  ): Promise<image.PixelMap> {
    await pixelMap.scale(
      targetWidth / pixelMap.getImageInfo().size.width,
      targetHeight / pixelMap.getImageInfo().size.height
    )
    return pixelMap
  }

  // 旋转图片
  static async rotate(pixelMap: image.PixelMap, degrees: number) {
    await pixelMap.rotate(degrees)
    return pixelMap
  }

  // 将 PixelMap 保存为 JPEG 文件
  static async saveAsJpeg(
    pixelMap: image.PixelMap,
    filePath: string,
    quality: number = 85
  ) {
    const packer = image.createImagePacker()
    const data = await packer.packing(pixelMap, {
      format: 'image/jpeg',
      quality: quality
    })
    // 写入文件(使用 fs 模块)
    const file = await fs.open(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY)
    await fs.write(file.fd, data)
    await fs.close(file.fd)
    packer.release()
  }
}

传感器 API

HarmonyOS NEXT 统一了各类传感器的访问接口:

import sensor from '@ohos.sensor'

@Entry
@Component
struct SensorDemo {
  @State accelerometerData: string = '等待数据...'
  @State stepCount: number = 0

  aboutToAppear() {
    this.startSensors()
  }

  aboutToDisappear() {
    this.stopSensors()
  }

  startSensors() {
    // 加速度计
    sensor.on(sensor.SensorId.ACCELEROMETER, (data) => {
      this.accelerometerData =
        `X: ${data.x.toFixed(2)}, Y: ${data.y.toFixed(2)}, Z: ${data.z.toFixed(2)}`
    }, { interval: 100 })  // 每 100ms 更新一次

    // 计步器
    sensor.on(sensor.SensorId.PEDOMETER, (data) => {
      this.stepCount = data.steps
    })
  }

  stopSensors() {
    sensor.off(sensor.SensorId.ACCELEROMETER)
    sensor.off(sensor.SensorId.PEDOMETER)
  }

  build() {
    Column({ space: 20 }) {
      Text('加速度计').fontSize(18).fontWeight(FontWeight.Bold)
      Text(this.accelerometerData).fontColor('#adbac7')

      Text('今日步数').fontSize(18).fontWeight(FontWeight.Bold)
      Text(this.stepCount.toString())
        .fontSize(48)
        .fontWeight(FontWeight.Bold)
        .fontColor('#CF0A2C')
    }
    .padding(24)
  }
}

地理位置

import geoLocationManager from '@ohos.geoLocationManager'

async function getCurrentLocation(): Promise<{ lat: number; lng: number }> {
  const request: geoLocationManager.CurrentLocationRequest = {
    priority: geoLocationManager.LocationRequestPriority.FIRST_FIX,
    scenario: geoLocationManager.LocationRequestScenario.UNSET,
    maxAccuracy: 100,  // 最大精度误差 100 米
    timeoutMs: 10000   // 10 秒超时
  }

  const location = await geoLocationManager.getCurrentLocation(request)
  return {
    lat: location.latitude,
    lng: location.longitude
  }
}

// 持续监听位置变化(如导航应用)
const subscriptionId = geoLocationManager.on('locationChange', {
  priority: geoLocationManager.LocationRequestPriority.ACCURACY,
  timeInterval: 1,       // 最少 1 秒更新一次
  distanceInterval: 5    // 移动超过 5 米更新
}, (location) => {
  console.log(`位置更新: ${location.latitude}, ${location.longitude}`)
})
传感器使用注意 传感器是高耗电设备,一旦不再需要必须立即关闭订阅(在 aboutToDisappear 或 onBackground 中调用 sensor.off())。在后台运行位置监听需要申请 LOCATION_IN_BACKGROUND 权限,且华为对此类行为审核严格,需提供合理的使用理由。