HarmonyOS鸿蒙Next中使用media.AVPlayer播放本地加密的MP3音频文件时,能否实现边解密边播放的效果?我们查阅相关资料得知该方案似乎不可行,但实际测试中发现:音频虽能开始播放,但声音还未播放完毕就提前终止。而该加密MP3文件在Andro

HarmonyOS鸿蒙Next中使用media.AVPlayer播放本地加密的MP3音频文件时,能否实现边解密边播放的效果?我们查阅相关资料得知该方案似乎不可行,但实际测试中发现:音频虽能开始播放,但声音还未播放完毕就提前终止。而该加密MP3文件在Andro 【问题描述】:

使用 media.AVPlayer 播放本地加密的 MP3 音频文件时,能否实现边解密边播放的效果?我们查阅相关资料得知该方案似乎不可行,但实际测试中发现:音频虽能开始播放,但声音还未播放完毕就提前终止。而该加密 MP3 文件在 Android 端可完整、正常播放。

【版本信息】:开发工具版本:6.0、手机系统版本:6.0、Api语言版本:20


更多关于HarmonyOS鸿蒙Next中使用media.AVPlayer播放本地加密的MP3音频文件时,能否实现边解密边播放的效果?我们查阅相关资料得知该方案似乎不可行,但实际测试中发现:音频虽能开始播放,但声音还未播放完毕就提前终止。而该加密MP3文件在Andro的实战教程也可以访问 https://www.itying.com/category-93-b0.html

6 回复

开发者您好,您可以边解密边把解密的数据写入AVPlayer的dataSrc中。如果问题仍然无法解决,请问是通过什么方法加密的mp3音频文件;另请提供一下加密MP3音频文件以及AVPlayer播放中途报错的日志,用于复现和定位问题。

更多关于HarmonyOS鸿蒙Next中使用media.AVPlayer播放本地加密的MP3音频文件时,能否实现边解密边播放的效果?我们查阅相关资料得知该方案似乎不可行,但实际测试中发现:音频虽能开始播放,但声音还未播放完毕就提前终止。而该加密MP3文件在Andro的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


开发者您好,提供如下demo实现边解密边播放:

  1. 通过异或方式的边解密边播放:
// Index.ets
import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';
import { Context } from '@kit.AbilityKit';

const XOR_KEY = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F];

class AVPlayerManager {
  private encryptFile?: fileIo.File;
  private avPlayer?: media.AVPlayer;
  private callback = (buffer: ArrayBuffer, length: number): number => {
    if (this.encryptFile === undefined) {
      return -2; // Error
    }
    try {
      let readSize = fileIo.readSync(this.encryptFile.fd, buffer);
      if (readSize === 0) {
        return -1; // End
      }
      let bufferView: Uint8Array = new Uint8Array(buffer);
      for (let i = 0; i < readSize; ++i) {
        bufferView[i] = bufferView[i] ^ XOR_KEY[i % 16];
      }
      return readSize;
    } catch (error) {
      console.error(`Failed to read file: ${JSON.stringify(error)}`);
      return -2; // Error
    }
  };

  async initPlayer(encryptFilePath: string) {
    // 创建AVPlayer
    if (this.avPlayer) {
      await this.release();
    }

    try {
      this.avPlayer = await media.createAVPlayer();
      this.setEventListening();
    } catch (err) {
      console.error(`create avplayer failed: ${err}`);
      return;
    }
    try {
      this.encryptFile = fileIo.openSync(encryptFilePath, fileIo.OpenMode.READ_WRITE);
      let fileSize = fileIo.statSync(this.encryptFile.fd).size;
      // 设置AVPlayer流式媒体资源描述
      this.avPlayer.dataSrc = { fileSize: fileSize, callback: this.callback };
    } catch (err) {
      console.error(`Failed to set avPlayer.dataSrc, Cause: ${JSON.stringify(err)}`);
    }
  }

  async release() {
    if (this.avPlayer) {
      try {
        await this.avPlayer.release();
        this.avPlayer = undefined;
      } catch (err) {
        console.error(`failed to invoke avplayer release, error is ${err}`);
      }
    }
    if (this.encryptFile) {
      try {
        fileIo.closeSync(this.encryptFile.fd);
        this.encryptFile = undefined;
      } catch (err) {
        console.error(`failed to close encrypt file, error is ${err}`);
      }
    }
  }

  private setEventListening() {
    this.avPlayer?.on('stateChange', async (state: media.AVPlayerState) => {
      if (this.avPlayer === undefined) {
        return;
      }
      switch (state) {
        case 'initialized':
          try {
            await this.avPlayer.prepare();
          } catch (err) {
            console.error(`failed to invoke avplayer prepare, error is ${err}`);
          }
          break;
        case 'prepared':
          try {
            await this.avPlayer.play();
          } catch (err) {
            console.error(`failed to invoke avplayer play, error is ${err}`);
          }
          break;
        default:
          console.info(`AVPlayer change to state: ${state}`);
          break;
      }
    });
  };
}

/**
 * 通过异或方式加密rawfile目录下的mp3文件,并将加密后文件保存到沙箱
 */
async function encryptMp3ByXOR(rawFilePath: string, sandFilePath: string, context: Context) {
  let sandFile: fileIo.File | undefined;
  try {
    sandFile = fileIo.openSync(sandFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
    let content: Uint8Array = context.resourceManager.getRawFileContentSync(rawFilePath);
    for (let i = 0; i < content.length; ++i) {
      content[i] = content[i] ^ XOR_KEY[i % 16];
    }
    fileIo.writeSync(sandFile.fd, content.buffer.slice(0));
    console.info(`Encrypt raw file and save to sandbox successfully`);
  } catch (error) {
    console.error(`Failed to encrypt mp3 file. ${JSON.stringify(error)}`);
  } finally {
    if (sandFile) {
      fileIo.closeSync(sandFile.fd);
    }
  }
}

@Entry
@Component
struct Index {
  private avPlayerMgr: AVPlayerManager = new AVPlayerManager();

  build() {
    Column({ space: 20 }) {
      Button('Encrypt')
        .fontSize(30)
        .onClick(() => {
          let context = this.getUIContext().getHostContext() as Context;
          let sandFilePath = context.filesDir + '/encrypt.mp3';
          encryptMp3ByXOR('test.mp3', sandFilePath, context);
        });

      Button('Play')
        .fontSize(30)
        .onClick(() => {
          let context = this.getUIContext().getHostContext() as Context;
          let filePath = context.filesDir + '/encrypt.mp3';
          this.avPlayerMgr.initPlayer(filePath);
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center);
  }
}
  1. 通过AES-CTR方式实现边解密边播放:
// Index.ets
import { media } from '@kit.MediaKit';
import { fileIo } from '@kit.CoreFileKit';
import { Context } from '@kit.AbilityKit';
import { cryptoFramework } from '@kit.CryptoArchitectureKit';

function genIvParamsSpec() {
  let ivData = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0]);
  let ivBlob: cryptoFramework.DataBlob = { data: ivData };
  let ivParamsSpec: cryptoFramework.IvParamsSpec = {
    iv: ivBlob,
    algName: 'IvParamsSpec',
  };
  return ivParamsSpec;
}

function genSymKeyByData(symKeyData: Uint8Array) {
  try {
    let symKeyBlob: cryptoFramework.DataBlob = { data: symKeyData };
    let aesGenerator = cryptoFramework.createSymKeyGenerator('AES128');
    let symKey = aesGenerator.convertKeySync(symKeyBlob);
    return symKey;
  } catch (error) {
    console.error(`Failed to generate key. Cause: ${JSON.stringify(error)}`);
    return undefined;
  }
}

const KEY_DATA =
  new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F]);
const SYM_KEY = genSymKeyByData(KEY_DATA);

class AVPlayerManager {
  private encryptFile?: fileIo.File;
  private avPlayer?: media.AVPlayer;
  private cipher?: cryptoFramework.Cipher;
  private decryptIdx: number = 0;
  private callback = (buffer: ArrayBuffer, length: number): number => {
    if (this.encryptFile === undefined) {
      return -2; // Error
    }
    if (this.cipher === undefined) {
      return -2; // Error
    }
    try {
      let readSize = fileIo.readSync(this.encryptFile.fd, buffer, { offset: this.decryptIdx, length: length });
      if (readSize === 0) {
        return -1; // End
      }
      let bufferData = new Uint8Array(buffer);
      let plainData = this.cipher.updateSync({ data: bufferData });
      this.decryptIdx += plainData.data.length;
      bufferData.set(plainData.data);
      return plainData.data.length;
    } catch (error) {
      console.error(`Failed to read file: ${JSON.stringify(error)}`);
      return -2; // Error
    }
  };

  async initPlayer(encryptFilePath: string) {
    // 创建AVPlayer
    if (this.avPlayer) {
      await this.release();
    }

    try {
      this.avPlayer = await media.createAVPlayer();
      this.setEventListening();
    } catch (err) {
      console.error(`create avplayer failed: ${err}`);
      return;
    }
    try {
      this.decryptIdx = 0;
      let ivParams = genIvParamsSpec();
      this.cipher = cryptoFramework.createCipher('AES128|CTR|NoPadding');
      this.cipher.initSync(cryptoFramework.CryptoMode.DECRYPT_MODE, SYM_KEY, ivParams);
    } catch (err) {
      console.error(`Failed to initialize cipher. Cause: ${JSON.stringify(err)}`);
      return;
    }
    try {
      this.encryptFile = fileIo.openSync(encryptFilePath, fileIo.OpenMode.READ_WRITE);
      let fileSize = fileIo.statSync(this.encryptFile.fd).size;
      // 设置AVPlayer流式媒体资源描述
      this.avPlayer.dataSrc = { fileSize: fileSize, callback: this.callback };
    } catch (err) {
      console.error(`Failed to set avPlayer.dataSrc, Cause: ${JSON.stringify(err)}`);
      return;
    }
  }

  async release() {
    if (this.avPlayer) {
      try {
        await this.avPlayer.release();
        this.avPlayer = undefined;
      } catch (err) {
        console.error(`failed to invoke avplayer release, error is ${err}`);
      }
    }
    if (this.encryptFile) {
      try {
        fileIo.closeSync(this.encryptFile.fd);
        this.encryptFile = undefined;
      } catch (err) {
        console.error(`failed to close encrypt file, error is ${err}`);
      }
    }
  }

  private setEventListening() {
    this.avPlayer?.on('stateChange', async (state: media.AVPlayerState) => {
      if (this.avPlayer === undefined) {
        return;
      }
      switch (state) {
        case 'initialized':
          try {
            await this.avPlayer.prepare();
          } catch (err) {
            console.error(`failed to invoke avplayer prepare, error is ${err}`);
          }
          break;
        case 'prepared':
          try {
            await this.avPlayer.play();
          } catch (err) {
            console.error(`failed to invoke avplayer play, error is ${err}`);
          }
          break;
        default:
          console.info(`AVPlayer change to state: ${state}`);
          break;
      }
    });
  };
}

/**
 * 通过AES-CTR方式加密rawfile目录下的mp3文件,并将加密后文件保存到沙箱
 */
async function encryptMp3ByXOR(rawFilePath: string, sandFilePath: string, context: Context) {
  let sandFile: fileIo.File | undefined;
  try {
    // Rand raw file
    sandFile = fileIo.openSync(sandFilePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE);
    let content: Uint8Array = context.resourceManager.getRawFileContentSync(rawFilePath);
    //
    let encryptIdx = 0;
    let ivParams = genIvParamsSpec();
    let cipher = cryptoFramework.createCipher('AES128|CTR|NoPadding');
    cipher.initSync(cryptoFramework.CryptoMode.ENCRYPT_MODE, SYM_KEY, ivParams);
    while (encryptIdx < content.length) {
      let cipherData = cipher.updateSync({ data: content.slice(encryptIdx, encryptIdx + 16) });
      for (let i = 0; i < cipherData.data.length; ++i) {
        content[encryptIdx] = cipherData.data[i];
        encryptIdx++;
      }
    }
    fileIo.writeSync(sandFile.fd, content.buffer.slice(0));
    console.info(`Encrypt Success`);
  } catch (error) {
    console.error(`Failed to encrypt mp3 file. ${JSON.stringify(error)}`);
  } finally {
    if (sandFile) {
      fileIo.closeSync(sandFile.fd);
    }
  }
}

@Entry
@Component
struct Index {
  private avPlayerMgr: AVPlayerManager = new AVPlayerManager();

  build() {
    Column({ space: 20 }) {
      Button('Encrypt')
        .fontSize(30)
        .onClick(() => {
          let context = this.getUIContext().getHostContext() as Context;
          let sandFilePath = context.filesDir + '/encrypt.mp3';
          encryptMp3ByXOR('test.mp3', sandFilePath, context);
        });

      Button('Play')
        .fontSize(30)
        .onClick(() => {
          let context = this.getUIContext().getHostContext() as Context;
          let filePath = context.filesDir + '/encrypt.mp3';
          this.avPlayerMgr.initPlayer(filePath);
        });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center);
  }
}

并没有解决,加密采用的AE这种

开发者您好,该问题正在处理中,请耐心等待。

在HarmonyOS鸿蒙Next中,使用media.AVPlayer播放本地加密MP3文件时,无法直接实现边解密边播放。AVPlayer要求输入标准的未加密音频数据流。当前测试中播放提前终止,是因为AVPlayer无法解析加密的音频帧数据。

根据你的描述,这是一个关于AVPlayer播放流式加密音频的典型问题。核心结论是:HarmonyOS Next的media.AVPlayer组件目前不支持对加密的MP3文件进行“边解密边播放”。

你遇到的“播放提前终止”现象,正是这一限制的直接表现。具体分析如下:

  1. AVPlayer的工作机制:AVPlayer在设计上主要用于播放标准的、完整的媒体文件或网络流。它期望接收到的数据源(无论是本地文件URI还是网络URL)是符合规范、可被系统媒体框架直接解码的格式。对于加密文件,AVPlayer无法识别其加密头或加密块,在尝试解码时遇到无法解析的数据,就会触发播放错误并终止。

  2. “能开始播放但提前终止”的原因:某些加密算法(如简单的头部混淆或块加密)可能不会破坏MP3文件的整体帧结构,AVPlayer在初始化时可能成功读取了部分未加密的元数据或有效的起始帧,因此播放得以开始。但随着播放进行,当AVPlayer读取到被加密的核心音频数据块时,由于无法解密,解码器会收到无效数据,导致解码失败,播放流程便被中断。

  3. 与Android方案的差异:在Android平台上实现此类功能,通常需要自定义MediaDataSource自定义解复用器(Extractor),在数据提供给MediaPlayer之前,在内存中进行实时解密。HarmonyOS Next的media API目前尚未提供同等灵活的低层数据注入接口(如setDataSource(MediaDataSource)),因此无法在播放管道中插入自定义的解密逻辑。

可行的替代方案建议

要实现加密音频的播放,必须在将数据交给AVPlayer之前完成解密。推荐路径如下:

  • 方案一:本地完整解密后播放 在播放前,将加密的MP3文件完整解密,保存为一个临时标准MP3文件,然后使用AVPlayer播放该临时文件。播放完成后及时清理临时文件。这是最可靠、兼容性最好的方案。

  • 方案二:使用更底层的音频API(如@ohos.multimedia.audio)自行实现播放流水线 如果对实时性要求极高,可以考虑:

    1. 读取加密文件流。
    2. 进行流式解密。
    3. 使用AudioRenderer将解密后的PCM数据直接送入音频设备播放。 但这需要自行处理MP3解码(可能需要集成第三方解码库)、音频队列管理、同步等复杂逻辑,实现难度和复杂度远高于使用AVPlayer。

总结: 当前在HarmonyOS Next上,使用标准media.AVPlayer直接播放加密的MP3文件并不可行。你观察到的现象符合预期。建议采用“先解密成临时文件,再播放”的方案作为当前阶段的解决方案。

回到顶部