Développement Multimédia HarmonyOS : Guide de Lecture et Capture Audio/Vidéo

Architecture Centrale du Sous-système Média

Le framework multimédia d'HarmonyOS 5 (API 12) repose sur une architecture en couches offrant une interface de service homogène. Ses piliers fondamentaux sont le lecteur AVPlayer, l'enregistreur AVRecorder et le module de capture d'écran AVScreenCapture, couvrant des besoins allant de la simple restitution sonore à la capture d'écran avancée.

Points forts du système

  • Moteur allégé : Optimisé pour consommer un minimum de ressources (threads, RAM), avec un pipeline modulable et extensible via plugins.
  • Support HDR natif : Capture et restitution HDR Vivid intégrées pour un rendu visuel supérieur.
  • Audio Pool : Technologie dédiée aux sons brefs, permettant un chargement unique et des lectures ultérieures à très faible latence.

Implémentation de la Lecture Audio/Vidéo

Fonctionnement d'AVPlayer

AVPlayer agit comme le composant de gestion principal du framework, prenant en charge les flux réseau et les fichiers locaux.

import media from '@ohos.multimedia.media';
import common from '@ohos.app.ability.common';

@Entry
@Component
struct MediaPlayerView {
  private playerInstance: media.AVPlayer | null = null;
  @State isMediaPlaying: boolean = false;
  @State currentPosition: number = 0;
  @State totalDuration: number = 0;

  async aboutToAppear() {
    await this.setupMediaEngine();
  }

  private async setupMediaEngine() {
    try {
      this.playerInstance = await media.createAVPlayer();

      await this.playerInstance.setSource({
        uri: 'https://example.com/sample.mp4',
        mediaType: media.MediaType.VIDEO
      });

      await this.playerInstance.prepare();
      this.totalDuration = await this.playerInstance.getDuration();

      this.playerInstance.on('timeUpdate', (elapsed: number) => {
        this.currentPosition = elapsed;
      });

      this.playerInstance.on('end', () => {
        this.isMediaPlaying = false;
      });

    } catch (err) {
      console.error('Échec d\'initialisation du lecteur : ', JSON.stringify(err));
    }
  }

  private async toggleMediaState() {
    if (!this.playerInstance) return;

    try {
      if (this.isMediaPlaying) {
        await this.playerInstance.pause();
      } else {
        await this.playerInstance.play();
      }
      this.isMediaPlaying = !this.isMediaPlaying;
    } catch (err) {
      console.error('Erreur de contrôle de lecture : ', JSON.stringify(err));
    }
  }

  private async jumpTo(timeMs: number) {
    if (!this.playerInstance) return;

    try {
      await this.playerInstance.seek(timeMs);
      this.currentPosition = timeMs;
    } catch (err) {
      console.error('Échec du saut : ', JSON.stringify(err));
    }
  }

  private async destroyEngine() {
    if (this.playerInstance) {
      await this.playerInstance.release();
      this.playerInstance = null;
    }
  }

  build() {
    Column({ space: 10 }) {
      VideoComponent({ avPlayer: this.playerInstance })
        .width('100%')
        .height(300)
        .backgroundColor('#000000')

      Row({ space: 5 }) {
        Button(this.isMediaPlaying ? 'Pause' : 'Lecture')
          .onClick(() => this.toggleMediaState())
          .width(80)

        Text(`${formatTime(this.currentPosition)}/${formatTime(this.totalDuration)}`)
          .fontSize(14)
          .textAlign(TextAlign.Center)
      }
      .margin(10)

      Slider({ value: this.currentPosition, min: 0, max: this.totalDuration })
        .onChange((val: number) => this.jumpTo(val))
        .width('90%')
    }
    .width('100%')
    .height('100%')
    .onDisappear(() => this.destroyEngine())
  }
}

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

Formats et Protocoles Supportés

Catégorie Spécifications Détails
Réseaux HTTP/HTTPS, HLS, HTTP-FLV Adapté au live et à la VOD
Audio AAC, MP3, VORBIS, PCM, AMR Conteneurs variés inclus
Vidéo H.264, H.265, MP4, MKV, TS Jusqu'à la résolution 4K

Implémentation de l'Enregistrement Audio/Vidéo

Capture Audio avec AVRecorder

AVRecorder permet des captures audio de haute qualité avec une configuration fine des paramètres d'encodage.

import media from '@ohos.multimedia.media';
import fileIo from '@ohos.file.fs';
import common from '@ohos.app.ability.common';

@Entry
@Component
struct SoundCaptureView {
  private recorderEngine: media.AVRecorder | null = null;
  @State isCapturingAudio: boolean = false;
  @State elapsedSeconds: number = 0;
  private saveDirectory: string = '';
  private tickInterval: number | null = null;

  async aboutToAppear() {
    await this.configureCaptureDevice();
  }

  private async configureCaptureDevice() {
    try {
      const appContext = getContext(this) as common.Context;

      this.saveDirectory = appContext.filesDir + '/saved_audios/';
      await fileIo.mkdir(this.saveDirectory, 0o755);

      this.recorderEngine = await media.createAVRecorder();

      const captureConfig: media.AVRecorderConfig = {
        audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
        outputFormat: media.OutputFormat.FORMAT_AAC_ADTS,
        audioEncoder: media.AudioEncoder.AUDIO_ENCODER_AAC,
        audioSampleRate: 44100,
        audioChannels: 2,
        audioBitrate: 128000
      };

      await this.recorderEngine.prepare(captureConfig);

    } catch (err) {
      console.error('Erreur de configuration du capteur : ', JSON.stringify(err));
    }
  }

  private async beginAudioCapture() {
    if (!this.recorderEngine) return;

    try {
      const uniqueName = `audio_${Date.now()}.aac`;
      const fullDestination = this.saveDirectory + uniqueName;

      await this.recorderEngine.start(fullDestination);
      this.isCapturingAudio = true;

      this.elapsedSeconds = 0;
      this.tickInterval = setInterval(() => {
        this.elapsedSeconds += 1;
      }, 1000);

    } catch (err) {
      console.error('Impossible de démarrer la capture : ', JSON.stringify(err));
    }
  }

  private async endAudioCapture() {
    if (!this.recorderEngine) return;

    try {
      await this.recorderEngine.stop();
      this.isCapturingAudio = false;

      if (this.tickInterval) {
        clearInterval(this.tickInterval);
        this.tickInterval = null;
      }

    } catch (err) {
      console.error('Erreur d\'arrêt de la capture : ', JSON.stringify(err));
    }
  }

  build() {
    Column({ space: 10 }) {
      Text('Démonstration d\'enregistrement')
        .fontSize(20)
        .margin(10)

      Text(`Durée : ${this.elapsedSeconds}s`)
        .fontSize(16)
        .margin(5)

      Button(this.isCapturingAudio ? 'Arrêter' : 'Enregistrer')
        .onClick(() => {
          if (this.isCapturingAudio) {
            this.endAudioCapture();
          } else {
            this.beginAudioCapture();
          }
        })
        .width(200)
        .margin(10)
    }
    .width('100%')
    .height('100%')
  }
}

Capture d'Écran (AVScreenCapture)

Pour les scénarios nécessitant l'enregistrement de l'écran, le module AVScreenCapture est requis :

import { BusinessError } from '@ohos.base';

class ScreenCatcher {
  private captureTool: any = null;
  private isActive: boolean = false;

  async setup() {
    try {
      this.captureTool = await media.createAVScreenCapture();

      const captureParams: media.AVScreenCaptureConfig = {
        captureMode: media.CaptureMode.CAPTURE_HOME_SCREEN,
        dataType: media.DataType.ORIGINAL_STREAM,
        audioInfo: {
          micCapInfo: {
            audioSampleRate: 48000,
            audioChannels: 2,
            audioSource: media.AudioSource.MIC
          }
        },
        videoInfo: {
          videoCapInfo: {
            videoFrameWidth: 1280,
            videoFrameHeight: 720,
            videoSource: media.VideoSource.SURFACE_RGBA
          }
        }
      };

      await this.captureTool.init(captureParams);
      await this.captureTool.setMicrophoneEnabled(true);

    } catch (fault) {
      console.error('Échec d\'initialisation de la capture d\'écran : ', (fault as BusinessError).message);
    }
  }

  async beginScreenRecord(filePath: string) {
    if (!this.captureTool) return;

    try {
      await this.captureTool.start(filePath);
      this.isActive = true;
    } catch (fault) {
      console.error('Échec de démarrage de la capture d\'écran : ', (fault as BusinessError).message);
    }
  }

  async terminateScreenRecord() {
    if (!this.captureTool) return;

    try {
      await this.captureTool.stop();
      this.isActive = false;
    } catch (fault) {
      console.error('Échec d\'arrêt de la capture d\'écran : ', (fault as BusinessError).message);
    }
  }
}

Gestion des Autorisations

L'accès aux fonctionnalités multimédia impose la déclaration préalable des permissions et leur requête dynamique à l'exécution.

Déclaration dans le Manifeste

Les autorisations doivent être listées dans le fichier module.json5 :

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:microphone_access_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.CAPTURE_SCREEN",
        "reason": "$string:screen_record_access_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      }
    ]
  }
}

Requête Dynamique

import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import bundleManager from '@ohos.bundle.bundleManager';
import { BusinessError } from '@ohos.base';

async function verifyAndObtainConsent(accessType: string, appContext: common.Context): Promise<boolean> {
  try {
    const accessCtrl = abilityAccessCtrl.createAtManager();
    const appData = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION);
    const clientToken = appData.appInfo.accessTokenId;

    const permissionState = await accessCtrl.checkAccessToken(clientToken, accessType);

    if (permissionState === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      return true;
    }

    const consentResult = await accessCtrl.requestPermissionsFromUser(appContext, [accessType]);
    return consentResult.authResults[0] === 0;

  } catch (fault) {
    console.error('Échec de vérification des droits : ', (fault as BusinessError).message);
    return false;
  }
}

Optimisation et Recommandations

Gestion de la Mémoire

  • Libérer immédiatement les instances de lecteur ou d'enregistreur inutilisées.
  • Ajuster la résolution et le débit vidéo pour limiter l'empreinte mémoire.
  • Mettre en place des mécanismes de préchargement et de cache.

Optimisation Énergétique

  • Suspendre les lectures en arrière-plan non essentielles.
  • Équilibrer qualité visuelle et consommation grâce à des paramètres d'encodage adaptés.
  • Exploiter le streaming à débit adaptatif (ABR) selon les conditions réseau.

Traitement des Anomalies

class MediaFaultResolver {
  static resolvePlaybackFault(fault: BusinessError): void {
    switch (fault.code) {
      case 5400103:
        console.error('Format média incompatible');
        break;
      case 5400104:
        console.error('Indisponibilité réseau');
        break;
      case 5400105:
        console.error('Échec du décodage');
        break;
      default:
        console.error('Anomalie inconnue : ', fault.message);
    }
  }

  static resolveCaptureFault(fault: BusinessError): void {
    // Logique de résolution des erreurs de capture
  }
}

Étude de Cas : Lecteur Vidéo Minimaliste

Voici une implémentation fonctionnelle d'un lecteur vidéo avec interface de contrôle :

import media from '@ohos.multimedia.media';
import common from '@ohos.app.ability.common';
import { BusinessError } from '@ohos.base';

@Entry
@Component
struct MinimalVideoPlayer {
  private corePlayer: media.AVPlayer | null = null;
  @State isControlPanelVisible: boolean = true;
  @State isMediaPlaying: boolean = false;
  @State currentPosition: number = 0;
  @State totalDuration: number = 0;
  private autoHideDelay: number | null = null;

  async aboutToAppear() {
    await this.bootPlayer();
  }

  private async bootPlayer() {
    try {
      this.corePlayer = await media.createAVPlayer();

      await this.corePlayer.setSource({
        uri: 'https://example.com/sample.mp4',
        mediaType: media.MediaType.VIDEO
      });

      await this.corePlayer.prepare();
      this.totalDuration = await this.corePlayer.getDuration();

      this.bindPlayerEvents();

    } catch (err) {
      console.error('Échec du démarrage du lecteur : ', JSON.stringify(err));
    }
  }

  private bindPlayerEvents() {
    if (!this.corePlayer) return;

    this.corePlayer.on('timeUpdate', (elapsed: number) => {
      this.currentPosition = elapsed;
    });

    this.corePlayer.on('end', () => {
      this.isMediaPlaying = false;
      this.currentPosition = 0;
    });

    this.corePlayer.on('error', (fault: BusinessError) => {
      console.error('Erreur de restitution : ', fault.message);
      this.isMediaPlaying = false;
    });
  }

  private switchPlayPause() {
    if (!this.corePlayer) return;

    if (this.isMediaPlaying) {
      this.corePlayer.pause();
    } else {
      this.corePlayer.play();
    }
    this.isMediaPlaying = !this.isMediaPlaying;
  }

  private async jumpToTime(targetMs: number) {
    if (!this.corePlayer) return;

    const validTarget = Math.max(0, Math.min(targetMs, this.totalDuration));
    await this.corePlayer.seek(validTarget);
    this.currentPosition = validTarget;
  }

  build() {
    Column() {
      Video({ avPlayer: this.corePlayer })
        .width('100%')
        .height(300)
        .onClick(() => {
          this.isControlPanelVisible = !this.isControlPanelVisible;
          this.restartAutoHide();
        })

      if (this.isControlPanelVisible) {
        Column() {
          Slider({
            value: this.currentPosition,
            min: 0,
            max: this.totalDuration,
            onChange: (val: number) => this.jumpToTime(val)
          })
          .width('90%')

          Row({ space: 20 }) {
            Button(this.isMediaPlaying ? 'Pause' : 'Lecture')
              .onClick(() => this.switchPlayPause())

            Text(`${formatTime(this.currentPosition)} / ${formatTime(this.totalDuration)}`)
              .fontSize(14)
          }
          .margin(10)
        }
        .backgroundColor('#CC000000')
        .padding(10)
      }
    }
    .width('100%')
    .height('100%')
  }

  private restartAutoHide() {
    if (this.autoHideDelay) {
      clearTimeout(this.autoHideDelay);
    }
    this.autoHideDelay = setTimeout(() => {
      this.isControlPanelVisible = false;
    }, 3000);
  }
}

Étiquettes: HarmonyOS AVPlayer AVRecorder ArkTS Multimédia

Publié le 10 septembre à 04h59