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);
}
}