HarmonyOS鸿蒙Next中能否对应用缓存目录中的文件进行边写边读的操作

HarmonyOS鸿蒙Next中能否对应用缓存目录中的文件进行边写边读的操作 我现在使用avplayer播放视频。但是avplayer不支持wmv或者mov格式的视频。那么我就使用了ffmpeg的三方库对这类视频进行转码操作。但是长时视频的转码消耗时间大。用户体验较差。那么能否做到对转码的输出视频文件进行边读边转码的操作?

PS:我尝试过使用ffmpeg的流式转码命令。类似于:

ffmpeg -i 输入文件路径 \
  -c:v libx264 \
  -c:a aac \
  -preset medium \
  -crf 23 \
  -movflags +faststart+frag_keyframe+empty_moov \  # 关键流式参数
  -f mp4 \                                         # 强制输出MP4
  -y \
  输出文件路径.mp4

但是使用avplayer读这个正在转码的文件时,会报错IO ERROE。所以我想问一下各位大佬。边转码边使用avplayer读数据播放的操作是否是可行的?如果不是可行的。那有没有好的处理方式能够优化avplayer播放这种本身不支持的视频格式的方法!!!


更多关于HarmonyOS鸿蒙Next中能否对应用缓存目录中的文件进行边写边读的操作的实战教程也可以访问 https://www.itying.com/category-93-b0.html

11 回复

建议直接使用更高级的播放器哦! ijkplayer 支持各种格式,不需要你再转码了!

https://gitcode.com/openharmony-sig/ohos_ijkplayer

安装:

ohpm install @ohos/ijkplayer

使用:

import { IjkMediaPlayer } from "@ohos/ijkplayer";
   import type { OnPreparedListener } from "@ohos/ijkplayer";
   import type { OnVideoSizeChangedListener } from "@ohos/ijkplayer";
   import type { OnCompletionListener } from "@ohos/ijkplayer";
   import type { OnBufferingUpdateListener } from "@ohos/ijkplayer";
   import type { OnErrorListener } from "@ohos/ijkplayer";
   import type { OnInfoListener } from "@ohos/ijkplayer";
   import type { OnSeekCompleteListener } from "@ohos/ijkplayer";
   import { LogUtils } from "@ohos/ijkplayer";

配置:

   XComponent({
      id: 'xcomponentId',
      type: 'surface',
      libraryname: 'ijkplayer_napi'
    })
    .onLoad((context) => {
      this.initDelayPlay(context);
     })
     .onDestroy(() => {
     })
     .width('100%')
     .aspectRatio(this.aspRatio)

播放:

//单例模式
    let mIjkMediaPlayer = IjkMediaPlayer.getInstance();
    //多实例模式
    let mIjkMediaPlayer = new IjkMediaPlayer();
    // 如果播放视频,调用setContext接口,参数1为XComponent回调的context, 可选参数2为XComponent的id属性值
    mIjkMediaPlayer.setContext(this.mContext, "xcomponentId");
    // 如果只播放音频,则调用setAudioId接口,参数为音频对象的id
    // mIjkMediaPlayer.setAudioId('audioIjkId');
    // 设置debug模式
    mIjkMediaPlayer.setDebug(true);
    // 初始化配置
    mIjkMediaPlayer.native_setup();
    // 设置视频源
    mIjkMediaPlayer.setDataSource(url); 
    // 设置视频源http请求头
    let headers =  new Map([
      ["user_agent", "Mozilla/5.0 BiliDroid/7.30.0 (bbcallen@gmail.com)"],
      ["referer", "https://www.bilibili.com"]
    ]);
    mIjkMediaPlayer.setDataSourceHeader(headers);
    // 使用精确寻帧 例如,拖动播放后,会寻找最近的关键帧进行播放,很有可能关键帧的位置不是拖动后的位置,而是较前的位置.可以设置这个参数来解决问题
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "enable-accurate-seek", "1");
    // 预读数据的缓冲区大小
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max-buffer-size", "102400");
    // 停止预读的最小帧数
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", "100");
    // 启动预加载
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", "1");
    // 设置无缓冲,这是播放器的缓冲区,有数据就播放
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", "0");
    // 跳帧处理,放CPU处理较慢时,进行跳帧处理,保证播放流程,画面和声音同步
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", "5");
    // 最大缓冲cache是3s, 有时候网络波动,会突然在短时间内收到好几秒的数据
    // 因此需要播放器丢包,才不会累积延时
    // 这个和第三个参数packet-buffering无关。
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", "3000");
    // 无限制收流
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", "1");
    // 屏幕常亮
    mIjkMediaPlayer.setScreenOnWhilePlaying(true);
    // 设置超时
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "timeout", "10000000");
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "connect_timeout", "10000000");
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "listen_timeout", "10000000");
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "addrinfo_timeout", "10000000");
    mIjkMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_timeout", "10000000");
    
    let mOnVideoSizeChangedListener: OnVideoSizeChangedListener = {
      onVideoSizeChanged(width: number, height: number, sar_num: number, sar_den: number) {
        that.aspRatio = width / height;
        LogUtils.getInstance()
          .LOGI("setOnVideoSizeChangedListener-->go:" + width + "," + height + "," + sar_num + "," + sar_den)
        that.hideLoadIng();
      }
    }
    mIjkMediaPlayer.setOnVideoSizeChangedListener(mOnVideoSizeChangedListener);
    let mOnPreparedListener: OnPreparedListener = {
      onPrepared() {
        LogUtils.getInstance().LOGI("setOnPreparedListener-->go");
      }
    }
    mIjkMediaPlayer.setOnPreparedListener(mOnPreparedListener);

    let mOnCompletionListener: OnCompletionListener = {
      onCompletion() {
        LogUtils.getInstance().LOGI("OnCompletionListener-->go")
        that.currentTime = that.stringForTime(mIjkMediaPlayer.getDuration());
        that.progressValue = PROGRESS_MAX_VALUE;
        that.stop();
      }
    }
    mIjkMediaPlayer.setOnCompletionListener(mOnCompletionListener);

    let mOnBufferingUpdateListener: OnBufferingUpdateListener = {
      onBufferingUpdate(percent: number) {
        LogUtils.getInstance().LOGI("OnBufferingUpdateListener-->go:" + percent)
      }
    }
    mIjkMediaPlayer.setOnBufferingUpdateListener(mOnBufferingUpdateListener);

    let mOnSeekCompleteListener: OnSeekCompleteListener = {
      onSeekComplete() {
        LogUtils.getInstance().LOGI("OnSeekCompleteListener-->go")
        that.startPlayOrResumePlay();
      }
    }
    mIjkMediaPlayer.setOnSeekCompleteListener(mOnSeekCompleteListener);

    let mOnInfoListener: OnInfoListener = {
      onInfo(what: number, extra: number) {
        LogUtils.getInstance().LOGI("OnInfoListener-->go:" + what + "===" + extra)
      }
    }
    mIjkMediaPlayer.setOnInfoListener(mOnInfoListener);

    let mOnErrorListener: OnErrorListener = {
      onError(what: number, extra: number) {
        LogUtils.getInstance().LOGI("OnErrorListener-->go:" + what + "===" + extra)
        that.hideLoadIng();
        prompt.showToast({
          message:"亲,视频播放异常,系统开小差咯"
        });
      }
    }
    mIjkMediaPlayer.setOnErrorListener(mOnErrorListener);

    mIjkMediaPlayer.setMessageListener();

    mIjkMediaPlayer.prepareAsync();

    mIjkMediaPlayer.start();

更多关于HarmonyOS鸿蒙Next中能否对应用缓存目录中的文件进行边写边读的操作的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


【解决方案】

HarmonyOS能够直接播放MOV格式的视频,可参考使用AVPlayer播放视频,但是需要注意的是,使用AVPlayer播放MOV格式视频时需要保证视频编码格式为H.264或HEVC(H.265)编码,当播放不支持的编码格式时会出现黑屏。

1、请确认下上面播放MOV格式视频的方案是否可以满足你的业务需求?

2、wmv格式视频播放也是您这边的必须场景吗?

为何不直接接入能直接播放这些格式的播放器

ijkplayer是OpenHarmony环境下可用的一款基于FFmpeg的视频播放器。

ohpm install @ohos/ijkplayer

参考地址

https://gitee.com/openharmony-sig/ohos_ijkplayer

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

但是该三方库要求的硬件环境是3568.我使用的是3588。会直接报错。

已解决了,

小伙伴你好,对转码输出文件“边写边读”再交给 AVPlayer 播放的方式整体并不稳定,建议直接使用成熟的第三方播放器库来兼容 WMV / MOV 等更多格式,会更可靠。

推荐方案

这两个播放器库内部已经集成了更丰富的解封装与解码能力,并对流式/渐进式播放等场景做了适配,相比自己用 FFmpeg 转码再喂给 AVPlayer,更适合当前这种原生不支持格式的播放需求。

建议转成HLS格式,用M3U+TS切片的方式来做。

Emby、Jellyfin这些服务端转码就是这么干的。

已知晓,但大佬说的这个涉及我的知识盲区了。我尝试过使用ffmpeg分片转码。就是一个视频按照每10s一个文件进行转码。然后顺序播放。但是每个分片直接播放会存在很明显间隙。且无法实现拖拽进度条播放。起码实现起来会特别特别复杂。我能力有限。做不出来。然后就是大佬说的方式我都没听懂…我已经用ijkPlayer这个三方库实现了我想要的功能了,

在HarmonyOS鸿蒙Next中,应用缓存目录支持边写边读操作。开发者可使用ArkTS的File API进行文件读写,通过异步I/O确保数据一致性。缓存目录路径可通过Context获取,读写时需注意文件锁和并发控制。系统对缓存文件无持久化保证,可能被自动清理。

在HarmonyOS Next中,对应用缓存目录的文件进行边写边读在技术上是可行的,这主要取决于文件系统的支持以及你如何管理文件访问。

核心可行性分析:

  1. 文件系统层面:HarmonyOS Next的应用沙盒目录(包括缓存目录)通常基于类似Linux的Ext4等文件系统。这类文件系统支持并发读写操作。这意味着一个进程(如你的转码任务)写入文件的同时,另一个进程(如AVPlayer)可以尝试读取该文件。
  2. 技术实现关键:问题的核心不在于操作系统是否允许,而在于读写操作的协调文件格式的完整性
    • 协调问题:你需要确保AVPlayer开始读取时,文件已经写入了一定的有效数据(例如,MP4的moov元数据盒子)。你使用的-movflags +faststart+frag_keyframe+empty_moov参数正是为了将元数据前置或分片,以支持流式播放,方向是正确的。
    • 格式问题:AVPlayer在打开文件时,会尝试解析文件头(如MP4的ftyp, moov盒子)。如果这些关键元数据尚未完全写入或位置不对,就会导致IO ERROR

针对你场景的具体建议:

  1. 检查FFmpeg输出与AVPlayer读取的时序IO ERROR很可能是因为AVPlayer在FFmpeg还未成功写入有效的文件头/初始片段时就尝试打开了文件。你需要实现一个简单的“生产者-消费者”协调机制:

    • 方案A(推荐):使用命名管道(FIFO)。让FFmpeg将输出写入一个命名管道,同时让AVPlayer从该管道读取。HarmonyOS Next的NDK支持POSIX API,可以创建和使用命名管道(mkfifo)。这种方式是标准的流式处理方案,能天然同步读写。
    • 方案B:如果必须使用文件,可以尝试先让FFmpeg快速写入一个包含有效moov元数据的、非常小的初始片段(利用-movflags参数),然后通知AVPlayer开始播放。后续数据以碎片形式(fragmented MP4)追加写入,AVPlayer应能持续读取。这需要仔细控制启动时机。
  2. 验证FFmpeg流式输出:在命令行中,你可以先将FFmpeg输出到标准输出(-f mp4 -),然后通过管道重定向到ffplay进行播放测试,以确认你的FFmpeg参数确实能生成可流式播放的数据流。

    ffmpeg -i input.wmv -c:v libx264 -c:a aac -preset ultrafast -crf 23 -movflags +frag_keyframe+empty_moov -f mp4 - | ffplay -
    

    如果上述命令能播放,证明转码流本身没问题,问题出在文件访问的协调上。

  3. 优化备选方案:如果边转边播的实现复杂度太高,考虑以下优化方向:

    • 分片转码与播放:将长视频按时间或关键帧分割成多个小片段(如5-10秒),转码完一个片段就立即提供给AVPlayer播放下一个片段,实现“准实时”播放。
    • 降低首帧延迟:使用更快的编码预设(如ultrafastsuperfast),虽然会增大文件体积或降低些许画质,但能极大提升转码速度,减少用户等待时间。
    • 预转码:对于已知的、可能重复播放的视频,考虑在后台或空闲时进行预转码并缓存。

总结: 在HarmonyOS Next中,技术上支持对缓存文件边写边读,但成功实现依赖于正确的流式媒体封装格式(如你使用的Fragmented MP4)和妥善的读写时序协调。建议优先尝试使用命名管道(FIFO) 来连接FFmpeg和AVPlayer,这是最符合流式处理范式且能避免文件锁竞争的方式。如果必须使用临时文件,则需要精确控制AVPlayer在初始元数据写入完成后再开始读取。

回到顶部