10-Flutter 鸿蒙实战 10:活化视频播放,video_player 与本地缓存

📅 2026/7/18 1:47:15
10-Flutter 鸿蒙实战 10:活化视频播放,video_player 与本地缓存
10. Flutter 鸿蒙实战 10活化视频播放video_player 与本地缓存不直连临时外链通过后端稳定接口下载视频再交给 video_player 播放。为什么需要本地缓存照片活化视频不是普通图片文件大、加载慢、外部链接可能过期。项目变更记录里有过一次真实问题故事详情页直接下载外部animatedUrl外部视频临时链接会 403 或过期导致 Android 和鸿蒙端都播放失败。后来项目改为通过自有后端GET /api/photos/:id/animated-video输出稳定视频再由客户端缓存到本地播放。StoriesRepository中负责视频缓存的字段staticfinalMapString,String_videoFileCache{};staticfinalMapString,FutureString?_videoFileRequests{};一个缓存最终文件路径一个缓存正在下载的请求。根据照片信息加载视频videoFileForPhoto会根据照片活化信息决定是否下载FutureString?videoFileForPhoto(StoryPhotoInfo?info)async{if(infonull||!info.shouldLoadAnimatedVideo||info.id.isEmpty){returnnull;}finalcacheKeyphoto:${info.id}:${info.animatedUrl.hashCode.toUnsigned(32).toRadixString(16)};finalcached_videoFileCache[cacheKey];if(cached!nullFile(cached).existsSync())returncached;finalpending_videoFileRequests[cacheKey];if(pending!null)returnpending;finalrequest_downloadVideoBytes(cacheKey,()_api.getBytes(/api/photos/${info.id}/animated-video),).whenComplete((){_videoFileRequests.remove(cacheKey);});_videoFileRequests[cacheKey]request;returnrequest;}cacheKey 包含照片 id 和视频地址 hash避免同一张照片更新视频后仍然使用旧缓存。临时目录缓存视频缓存放到系统临时目录finalcacheDirectoryDirectory(${Directory.systemTemp.path}${Platform.pathSeparator}story_videos,);临时缓存适合这类可重新下载的资源。它不是用户原始数据丢失后可以重新从后端拉取。详情页播放故事详情页引入了video_playerimportpackage:video_player/video_player.dart;当本地视频路径准备好后可以用VideoPlayerController.file(File(path))初始化。播放控件要处理加载中、失败、播放、暂停、进度等状态。还要注意一个体验细节用户开始播放视频前应暂停故事语音避免两个声音同时播放。活化状态轮询活化视频可能处于 processing、caching、done、failed 等状态。详情页通过_syncAnimationPolling(_photoInfo)同步轮询逻辑。视频未准备好时不能假装完成应展示真实状态视频加载失败后刷新照片状态让后端有机会重新排队或修复缓存。插件注册pubspec.yaml中的 video_player 使用 OpenHarmony 适配分支video_player:git:url:https://gitcode.com/openharmony-tpc/flutter_packages.gitref:br_video_player-v2.9.2_ohospath:packages/video_player/video_player鸿蒙侧注册flutterEngine.getPlugins()?.add(newVideoPlayerPlugin());如果只改 Dart 依赖、不生成或注册鸿蒙插件HAP 构建或运行时都会出问题。视频来源治理端内播放要关注资源来源视频播放的难点不只是VideoPlayerController更重要的是视频从哪里来。项目一开始直接使用外部animatedUrl后来因为 403 和临时链接失效改成后端稳定视频接口。()_api.getBytes(/api/photos/${info.id}/animated-video)移动端只信任自己的后端接口后端再去处理第三方链接、缓存和修复。这样权限、错误提示、重试逻辑都更可控。后端视频接口比临时链接更稳定animationStatusdone后来被修正为“服务器已缓存视频”。这个改动的核心不是改文案而是把端内播放依赖从第三方临时链接转到自有后端接口。_api.getBytes(/api/photos/${info.id}/animated-video)这样移动端只关心自己的后端是否能返回视频字节。第三方链接过期、403、重试和缓存修复都交给后端处理App 侧的错误分支会少很多。视频本地化播放视频资源为什么要先落本地移动端视频播放通常不适合直接拿远端临时链接反复播放。链接可能过期网络可能抖动播放器也更适合稳定的本地文件。这个项目把视频字节下载到临时目录再交给video_player播放。FutureString?videoFileForPhoto(StoryPhotoInfo?info)async{if(infonull||!info.shouldLoadAnimatedVideo||info.id.isEmpty){returnnull;}finalcacheKeyphoto:${info.id}:${info.animatedUrl.hashCode.toUnsigned(32).toRadixString(16)};finalcached_videoFileCache[cacheKey];if(cached!nullFile(cached).existsSync())returncached;...}缓存 key 同时包含照片 id 和视频地址 hash。视频地址变化后旧缓存不会被误用同一视频重复打开时又能直接复用本地文件。状态语义要以端内可播放为准服务端说视频已生成不代表端内一定能播放。移动端还要考虑下载失败、文件损坏、播放器初始化失败等情况。所以详情页需要独立维护视频文件路径和播放控制器状态。服务端状态processing / ready / failed 端内状态downloading / cached / initializing / playable / error这两组状态不能混成一个字段。服务端状态说明任务有没有完成端内状态说明当前设备能不能播放。用户看到的提示应该结合两者判断。下载失败不能影响故事详情视频是附加能力故事正文、照片、年代地点才是主内容。项目在prepareStoryDetail里预取视频时捕获异常不让视频失败阻止详情页打开。if(videoFilePathnullinfo?.shouldLoadAnimatedVideotrue){try{videoFilePathawaitvideoFileForPhoto(info);}catch(_){// 详情页会刷新照片状态并显示可读状态预取失败不阻断打开。}}这个设计和上传后处理的原则一致主内容优先可用附加能力单独展示状态。视频失败时用户仍然能读故事、听语音、编辑信息。video_player 的鸿蒙依赖video_player必须使用带 OpenHarmony 实现的版本并且注册文件里要能看到VideoPlayerPlugin。video_player:git:url:https://gitcode.com/openharmony-tpc/flutter_packages.gitref:br_video_player-v2.9.2_ohospath:packages/video_player/video_playerflutterEngine.getPlugins()?.add(newVideoPlayerPlugin());如果 Dart 代码里引入了video_player但鸿蒙侧没有插件实现或没有注册构建或运行都会出问题。插件依赖、生成注册、真机播放要一起验证。视频功能验证路径1. 没有视频时详情页不显示错误的播放入口。 2. 视频生成中时页面显示处理中状态。 3. 视频可用时先下载到本地再初始化播放器。 4. 断网后已缓存视频仍能复用。 5. 服务端链接失效时错误只影响视频区域不影响故事正文。 6. 离开详情页后释放播放器资源。视频播放链路比图片更长问题也更隐蔽。把服务端任务状态、本地缓存状态、播放器状态拆开后续排查会清楚很多。视频播放排查视频缓存属于临时资源活化视频可以从服务端重新下载因此适合放在临时目录不需要进入用户长期数据目录。临时缓存丢失后重新拉取即可。适合长期保存用户上传的原始照片、本地草稿 适合临时缓存视频播放文件、图片缩略图、接口派生资源这个边界能避免 App 占用越来越大。后续如果要做缓存清理也能优先清理视频和缩略图不影响用户原始数据。下载请求也要合并故事详情可能多次触发视频加载。如果同一个视频正在下载应该复用正在进行的 Future而不是并发下载多次。finalpending_videoFileRequests[cacheKey];if(pending!null)returnpending;finalrequest_downloadVideoBytes(cacheKey,()_api.getBytes(/api/photos/${info.id}/animated-video),);_videoFileRequests[cacheKey]request;视频文件通常比图片大得多并发重复下载会浪费流量也容易让播放器拿到不完整文件。请求合并在视频场景里比图片更重要。轮询要有边界视频活化可能需要等待详情页可以轮询状态但不能无限轮询。轮询要随着页面销毁停止也要在状态完成或失败后停止。Timer?_animationPollTimer;overridevoiddispose(){_animationPollTimer?.cancel();_audioPlayer.dispose();super.dispose();}如果离开页面后轮询还在继续就会浪费网络并可能在页面销毁后触发状态更新。移动端长任务一定要处理生命周期。播放失败的排查顺序视频无法播放时按下面顺序排查效率更高1. 照片详情接口里视频状态是否 ready。 2. /api/photos/{id}/animated-video 是否能返回字节。 3. 本地临时文件是否写入成功。 4. video_player 插件是否注册。 5. VideoPlayerController 是否初始化成功。这个顺序从服务端状态到端内播放逐层推进。直接改播放器 UI 往往解决不了根因。视频播放问题定位表现象优先检查对应文件处理方向视频入口不显示照片状态是否允许加载StoryPhotoInfo检查shouldLoadAnimatedVideo视频下载失败字节接口是否可用stories_repository.dart、api_client.dart检查/animated-video接口和 token视频能下载但不能播放插件和控制器是否正常video_player依赖、详情页检查插件注册和 Controller 初始化多次进入重复下载视频请求是否合并_videoFileRequests同一 cacheKey 复用 pending Future视频链路比图片多了本地文件和播放器初始化两个环节。排查时要区分“拿不到视频字节”和“拿到字节但播放器播不了”。视频文件命名策略视频下载到本地后文件名不能只用照片 id。因为同一张照片可能重新生成视频旧视频地址和新视频地址不同。如果文件名只按照片 id 命名重新生成后可能继续播放旧文件。photoId animatedUrlHash - 本地视频文件名这个策略和项目里的 cacheKey 思路一致。照片 id 解决归属问题地址 hash 解决版本问题。重新生成视频后 hash 变化本地缓存自然失效。器初始化两个环节。排查时要区分“拿不到视频字节”和“拿到字节但播放器播不了”。