做Flutter视频播放最折磨人的问题之一,就是视频源的横向竖向。这个坑不只是“容器没写好”那么简单。我做短视频播放模块时,遇到过同一条视频在A手机上横屏满屏、在B手机上竖着显示人像被拉长的情况;也遇到过明明素材是竖的,播放器先闪一下横的画面再纠正;还有一次是视频方向完全躺倒了,折腾半天才发现是播放器内核没有处理文件的旋转标记。
排查完一圈你会发现,方向错乱的真凶藏在三个地方:视频文件本身的元数据、播放器底层解码时有没有把旋转信息应用上、以及Flutter侧布局容器的适配方式。这篇我就按这三层逐层拆,把判断代码、自适应容器、旋转修正和横竖屏切换的完整方案都整理出来,适合正在做短视频、直播回放、视频号类App的Flutter开发者直接参考,少走我踩过的弯路。
1. 问题的表象:视频源方向错乱到底长什么样
先别急着写代码,先搞清楚你遇到的到底是哪种“方向不对”。我在群里看大家问问题,经常发现两个人说的是完全不同的bug,结果给出的建议南辕北辙。
我按实际项目里遇到的情况,把方向问题分成五类,你可以对着自查:
| 症状 | 现象描述 | 最容易怀疑的原因 |
|---|---|---|
| 拉伸变形 | 横向视频被塞进竖屏容器,人物被拉成长条 | UI层宽高比没有跟随视频实际尺寸 |
| 旋转90度 | 画面“躺倒”,人脸朝左或朝右 | 播放器没有应用旋转元数据 |
| 上下颠倒 | 画面翻转180度 | rotate=180的元数据被播放内核忽略 |
| 先横后竖 | 刚加载瞬间闪一下横画面,初始化完成后才变正 | 首帧前用了错误的占位比例 |
| 黑边或裁切 | 方向正确,但内容显示不全或四周留黑 | BoxFit选择不当或容器比例过死 |
这里有个细节:“先横后竖”这个症状最容易被忽略。用户感知是“方向错乱”,但代码层面视频最终的方向是对的,只是加载过程中的短暂闪烁让人印象很差。这类问题通常不在解码层,而在占位图和首帧渲染的衔接上,后面第六章我会专门说。
还有一种容易误判的情况:视频在列表页小窗口里方向正常,点进全屏播放后反而被拉伸了。这种不是视频方向问题,而是全屏模式下容器比例变了,但视频组件没有重新适应。方向问题一旦出现,先按照上面的表格归类,再往下定位,效率会高很多。
2. 根因分析:视频方向由谁决定——元数据、解码器还是UI层
要解决方向问题,得先理解视频文件里方向和尺寸到底存在哪。这一层搞不清,后面所有代码都是瞎调。
2.1 视频文件的“三张脸”:容器、像素流和旋转标记
一个视频文件拆开看,大致包着两样东西:编码后的像素数据流和容器层的元数据。
像素流是什么?就是你看到的每一帧画面,解出来之后是一堆RGB/YUV数据,它有固定的宽和高。比如横屏拍摄的视频,像素流是1920x1080;竖屏拍摄的视频,像素流期望是1080x1920——但这里就是猫腻所在。
很多手机摄像头因为传感器物理方向固定的原因,你竖着拿手机拍视频,底层记录的像素流其实还是横着的1920x1080,画面是躺倒的。这时视频文件会在元数据里写一个旋转标记(rotate),比如写rotate=90,播放器读到这个标记,就把画面顺时针旋转90度,最终呈现给你竖屏效果。
也就是说,“视频本身是横向还是竖向”这个说法很容易误导人。像素流方向和最终显示方向可以不同,中间差着一个旋转标记。剪辑软件导出视频时,通常会把旋转标记烘焙进像素里(重新编码),所以导出的视频rotate=0,像素流本身就是正的;而手机直接录制的视频,很多只是“标记方向”,没有真正转正像素。
2.2 播放器到底做了什么:解码、旋转、渲染的三步流水
播放器的工作流可以简化成三步:
- 解封装,读取容器里的像素流信息和元数据;
- 解码像素流,得到原始帧数据;
- 根据元数据里的旋转标记,对帧做旋转,再交给渲染层显示。
常见的系统播放器,比如Android系统自带的播放能力、iOS的AVPlayer,都会自动执行第三步。所以你在手机相册里看视频,方向永远是正常的。但不是所有播放器内核都会执行旋转,尤其是一些自研内核只解像素不管元数据,直接就把躺倒的原始帧扔给你,画面自然就错了。
2.3 反直觉的结论:video_player给你的size是可以信任的
用Flutter官方video_player插件时,很多人会担心一个事:它给的controller.value.size到底是Pixel宽高还是带旋转后的显示宽高?会不会被元数据坑了?
直接说结论:不用怕,video_player底层调用的是ExoPlayer和AVPlayer,这两个底层播放器都会自动应用旋转标记。controller.value.size是旋转完成之后的显示尺寸,也就是说,如果你看到size是1080x1920,那视频确实该竖着放;看到1920x1080,就该横着放。用它做布局处理,天然正确。
真正出问题的场景,往往是你在Flutter里没有用video_player的VideoPlayer组件,而是自己把解码后的Texture帧取出来,塞进RawImage或者自定义渲染管线里。这个时候拿到的是解码buffer,旋转标记有没有被应用完全取决于你的渲染层有没有处理,绕过了底层播放器,方向自然容易翻车。
我建议:能用组件就用组件,不要自己接Texture层,除非你有明确的定制需求。方向问题会随自定义渲染结构被大量引入。
2.4 UI层只决定“容器多大”,不决定“画面方向”
Flutter侧的尺寸布局,本质上是回答“这个播放区域占多大面积”的问题。你用AspectRatio、SizedBox、Expanded去约束容器,只影响视频显示的区域比例,跟视频内容的方向没有任何关系。
但很多时候,UI层又会被误判成“方向问题”:你给一个横版视频容器用了竖屏的宽高比,视频被拉伸填满,人像变长了。这种拉伸不是视频方向错,而是容器和视频宽高比不匹配。理解这一层,后面做自适应容器时就不会被表象带偏。
3. 拿到一个视频源,怎么快速判断它该横着放还是竖着放
定位问题时需要先确认一件事:这个视频源,在正确播放的前提下,到底该横放还是竖放?有两种途径可以确认,一个是在Flutter侧读显示尺寸,一个是用ffprobe看底层元数据。两条都能用,但适用时机不同。
3.1 首选判断路径:在Flutter侧读取显示尺寸
实践中用video_player时,等播放器初始化完成再读取尺寸就够了:
_controller = VideoPlayerController.networkUrl(Uri.parse(widget.url)); _controller.addListener(() { if (_controller.value.isInitialized && !_hasLogged) { _hasLogged = true; final size = _controller.value.size; final ratio = size.width / size.height; debugPrint('显示尺寸: $size, 宽高比: ${ratio.toStringAsFixed(3)}'); debugPrint('方向: ${ratio > 1 ? "横向" : ratio < 1 ? "竖向" : "方形"}'); } }); await _controller.initialize();这段代码核心就是一件事:拿到显示尺寸,算宽高比,判断方向。注意两点:一是监听器里必须有isInitialized判断,不然size是0;二是_hasLogged这个开关防止反复打印。
在初始化完成之前,controller.value.size是Size.zero,这时候如果直接拿去做布局,AspectRatio会计算出0或者无限大,轻则控制台刷红,重则布局直接崩掉。所以任何读取尺寸的代码,都必须放在初始化回调之后。
3.2 辅助诊断方法:ffprobe看像素宽高和旋转标记
如果你想确认视频源是不是“像素流方向与显示方向不一致”的特殊情况,可以在电脑上装FFmpeg,用ffprobe看底层信息:
ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height:stream_side_data=rotation \ -of default=noprint_wrappers=1 input.mp4输出大致长这样:
width=1920 height=1080 rotation=90这里的信息和Flutter侧看到的size不一样:width/height是像素排列的原始宽高,rotation是播放器应该应用的角度。上面这个例子,像素流是横的1920x1080,但rotation=90,所以最终显示方向是竖向1080x1920。
如果某天你用某个播放器播放这种视频得到横屏躺倒的画面,排查思路立刻就清晰了:不是视频文件坏,是播放器没有应用rotation=90这个标记。这种跨层定位能力,在方向问题排查中非常实用。
3.3 横竖判断的宽度阈值与方形视频边界
判断方向时别太死板,直接用ratio > 1和ratio < 1来二选一,遇到接近方形的视频很容易判断错。我给一个经验阈值:
- ratio > 1.2:横向视频;
- ratio < 0.83:竖向视频;
- 0.83到1.2之间:看成“方形或接近方形”视频,用contain模式留白显示,不要强行拉伸填满。
为什么这么设?因为视频比例普遍是16:9(即1.78)、9:16(0.56)、4:3(1.33)、3:4(0.75)、1:1(1.0)。1.2和0.83正好落在1:1附近,能避免把1:1视频跟横竖方向靠边。短视频平台现在也有大量1:1的内容,这种视频在列表页和全屏页的适配思路跟横竖屏都不一样,先归类好再处理。
4. UI布局实战:让播放器容器自适应横屏与竖屏视频
判断完方向,下一步就是让播放器的容器跟着视频比例走。这节我给两套方案:单视频播放页的自适应方案、列表页多视频混合时的统一Viewport方案。两套我都跑过实际项目,可以直接抄。
4.1 自适应容器的核心:AspectRatio是给视频播放量身定做的
单个视频页里,最简方案就是把视频的真实宽高比传给AspectRatio,让容器和视频永远保持一致:
class AdaptiveVideoPlayer extends StatefulWidget { final String url; const AdaptiveVideoPlayer({super.key, required this.url}); @override State<AdaptiveVideoPlayer> createState() => _AdaptiveVideoPlayerState(); } class _AdaptiveVideoPlayerState extends State<AdaptiveVideoPlayer> { late VideoPlayerController _controller; bool _isReady = false; @override void initState() { super.initState(); _controller = VideoPlayerController.networkUrl(Uri.parse(widget.url)); _controller.initialize().then((_) { if (!mounted) return; setState(() => _isReady = true); }); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (!_isReady) { return const AspectRatio( aspectRatio: 16 / 9, child: ColoredBox(color: Colors.black), ); } final ratio = _controller.value.size.width / _controller.value.size.height; return AspectRatio( aspectRatio: ratio, child: VideoPlayer(_controller), ); } }核心就一句话:用视频显示尺寸的宽高比去驱动AspectRatio。这样无论横屏还是竖屏,视频都会被按正确比例渲染,不会拉伸。
说一个实际项目的细节:初始化完成之后立刻setState让播放器挂载,但这时候画面可能还没真正渲染出来,会有一段黑屏。常见做法是用一个loading转圈顶着,直到首帧纹理出现再切走。video_player的value.isPlaying和首帧回调不是一回事,我自己为了首帧做了个占位管理,在4.4节细说。
4.2 列表页混合方向视频:统一Viewport+contain留白
单视频页面好办,麻烦的是信息流列表:一屏里横版视频、竖版视频、方形视频混着来。如果每个item的高度都跟着视频比例走,整个列表高度会非常跳跃,用户滑起来像在坐跳楼机。
我的做法是外层容器统一高度,内层用Center+AspectRatio让视频在固定区域里等比显示,超出部分留黑:
class VideoListItem extends StatelessWidget { final VideoPlayerController controller; final double viewportHeight; const VideoListItem({ super.key, required this.controller, required this.viewportHeight, }); @override Widget build(BuildContext context) { final ratio = controller.value.size.width / controller.value.size.height; return SizedBox( height: viewportHeight, width: double.infinity, child: Center( child: AspectRatio( aspectRatio: ratio, child: VideoPlayer(controller), ), ), ); } }列表容器高度统一(比如320),横屏视频宽度比较宽,高度受限后上下留一点黑边;竖屏视频显示在中央,左右留黑。这跟B站、YouTube信息流里的效果一致,观感上比强行拉伸干净得多。
这里有个取舍:列表页为了沉浸感,很多人会把竖屏视频直接裁满全屏,用cover模式裁掉上下内容。短视频里这招常见,但裁切要看内容,字幕、人脸、重点动作被切掉就得不偿失了。稳妥起见,列表页用contain保证完整,进入详情页全屏后再按需调整。
4.3 BoxFit的选择:contain保证完整,cover制造氛围感
VideoPlayer组件本身内部渲染模式不是随便调的,但如果是用Image做封面或背景,就会遇到BoxFit的选择。
| BoxFit值 | 行为 | 适用场景 |
|---|---|---|
| BoxFit.contain | 等比完整显示,留黑边 | 播放器主体、带字幕/重点内容的覆盖图 |
| BoxFit.cover | 等比填充,裁掉超出部分 | 短视频背景、氛围封面、列表缩略图 |
| BoxFit.fill | 拉伸填满,比例失真 | 几乎不用,除非画面内容不敏感 |
| BoxFit.fitWidth / fitHeight | 保持比例,按宽或高对齐 | 横屏视频固定宽度、竖屏视频固定高度 |
我的习惯是:播放器主体永远用contain,背景层才用cover。短视频玩法常见的“模糊背景+居中完整画面”就是这个逻辑——背景图放大模糊,中间视频用contain显示,既避免了黑边,又保住了完整画面,观感高级很多。
4.4 占位图比例影响“首帧跳变”:别小看这个小细节
前面提到“先横后竖”的闪烁,很多时候就是占位图和视频实际比例不一致造成的。
假设占位图用的是16:9的灰色块,视频加载完成发现实际是9:16,这时候容器高度突然从16:9切换到9:16,用户会看到画面剧烈跳动。尤其是在列表快速滑动时,这种跳变会让列表严重闪屏。
解决思路是:占位图先别急着用固定比例,等拿到实际比例之后再切占位。但问题来了,拿比例必须等初始化完成,这跟“加载中”的时机是冲突的。
我的折中做法分两级:
- 首帧之前,用封面图(如果有封面图接口)按封面图比例占位,因为封面图通常和视频内容构图一致;
- 初始化完成且拿到视频宽高比后,先做一个200ms左右的尺寸动画过渡,再挂载VideoPlayer组件,让用户感知是“平滑放大/缩小”,而不是“突然蹦一下”。
尺寸动画用AnimatedContainer或者TweenAnimationBuilder就能实现,代码不复杂,但交互体验提升非常明显。
5. 旋转处理:视频画面角度不对时,该转视频还是转播放器
前面讲的都是“视频源方向本身正常”的情况。接下来是真正头大的:视频源的旋转标记有问题,或者播放器内核不处理旋转,导致画面躺倒。这时候需要在Flutter侧做画面旋转修正。
5.1 先判断是哪一层错了:什么时候才真的需要自己出手
在动手旋转之前,先定位是哪个环节出的问题。我常用的排查思路是:同一段视频,分别用系统相册/电脑播放器、Flutter的video_player、以及你自己的自定义播放器放一遍,看哪个方向是错的。
- 如果只有自定义播放器错,说明解码/渲染层没处理旋转,优先补播放器内核;
- 如果所有播放器都错,说明视频源本身的旋转标记就是错的或没有标记,这种情况要么找产品确认素材标准,要么在服务端/上传端做视频转正;
- 如果只有Flutter层某些页面对,某些页面错,那多半是页面容器或Transform状态残留。
Flutter侧UI旋转是最后手段,不是第一方案。因为它解决的是“显示层纠正”,底层数据没变,同一段视频换个入口播放可能又是错的,属于打补丁。
5.2 Flutter侧画面旋转:Transform.rotate与RotatedBox的差别
如果确实需要在Flutter侧旋转,先分清两个组件:
RotatedBox会在布局阶段就交换子组件的宽高约束,它改变的是“布局尺寸”,旋转90度后,原来的宽度会变成高度,占位空间也跟着换。
Transform.rotate只是把子组件画到画布上时做旋转,不会影响布局尺寸。这是它最大的坑:旋转90度后,画面超出原容器边界,直接裁掉,或者留下大片空白。
看这个例子:一个16:9的横屏视频,你想在竖屏容器里旋转成9:16展示,直接套Transform.rotate是行不通的:
// 错误示例:画面会被裁剪 SizedBox( width: 200, height: 356, child: Transform.rotate( angle: math.pi / 2, child: VideoPlayer(controller), ), );旋转之后,视频的宽高方向对调了,但容器布局还按原宽高走,结果是画面被上下裁剪,中间只露出一条,完全没法看。
正确做法是旋转之后,外层容器的宽高也要跟着对调:
// 正确思路:容器宽高比先“反转” SizedBox( width: 356, height: 200, child: Transform.rotate( angle: math.pi / 2, child: VideoPlayer(controller), ), );也就是旋转90度时,屏幕上的宽高要互换。实际操作中,我会用一个辅助方法算出旋转后视频的实际显示尺寸,再往Widget树里传:
double getRotatedVideoWidth(Size originalSize, int angleDeg) { if (angleDeg % 180 == 0) return originalSize.width; return originalSize.height; } double getRotatedVideoHeight(Size originalSize, int angleDeg) { if (angleDeg % 180 == 0) return originalSize.height; return originalSize.width; }这套计算不复杂,但特别容易被忽略,我在这上面翻过车。旋转180度和0度宽高不变,只有90度和270度需要互换。
5.3 旋转90度后容器布局的完整修正:一个可直接用的组件
整理一下,一个能处理任意旋转角度的通用播放容器大概长这样:
class RotableVideoPlayer extends StatelessWidget { final VideoPlayerController controller; final int angleDeg; // 0, 90, 180, 270 const RotableVideoPlayer({super.key, required this.controller, required this.angleDeg}); @override Widget build(BuildContext context) { final size = controller.value.size; final displayWidth = angleDeg % 180 == 0 ? size.width : size.height; final displayHeight = angleDeg % 180 == 0 ? size.height : size.width; return AspectRatio( aspectRatio: displayWidth / displayHeight, child: Transform.rotate( angle: angleDeg * math.pi / 180, child: VideoPlayer(controller), ), ); } }旋转180度用Transform.rotate没问题;旋转90度或270度时,外层容器已经把宽高比调成反转后的比例,画面就不会被裁切了。
注意:如果视频本身是竖屏(比如1080x1920),但你想让它在横屏容器里全屏显示,旋转90度后虽然画面方向对了,但画幅比例可能仍和容器不一致,这时候需要配合scale放大填满。我在项目中用LayoutBuilder动态算缩放系数,放一个极简思路:
final scaleFactor = math.max( constraints.maxWidth / displayWidth, constraints.maxHeight / displayHeight, );把Transform.scale包在旋转外面,放大到填满容器即可。但这招会裁掉边缘内容,注意视频构图。
5.4 更省心的预处理方案:ffmpeg转正,从源头消除隐患
如果你能控制视频源(比如是自家平台上传的视频),最好的方案不是播放器里旋转,而是上传/转码链路里直接转正视频。
ffmpeg有两条路,效果不同:
只去掉旋转标记,保留原始像素,速度极快(几乎不转码):
ffmpeg -i input.mp4 -metadata:s:v rotate=0 -codec copy output.mp4这条命令很诱人,因为几乎瞬间完成。但它有个隐患:如果播放器不认rotate标记,它会直接播放原始像素——原本旋转90度标记的视频,去掉rotate=0后,播放器得到的是横躺的原始画面。换句话说,去掉标记只是“让认标记的播放器不再旋转”,并不能让不认标记的播放器变正常。
真正稳妥的做法是把方向烘焙进像素,彻底转码:
# 顺时针旋转90度:适合原标记为“逆时针90度”的视频 ffmpeg -i input.mp4 -vf "transpose=clockwise" -c:a copy output.mp4 # 逆时针旋转90度 ffmpeg -i input.mp4 -vf "transpose=cclock" -c:a copy output.mp4ffmpeg里transpose滤镜的参数如下:
| 参数 | 效果 |
|---|---|
| cclock_flip | 逆时针90度并垂直翻转 |
| clock | 顺时针90度 |
| cclock | 逆时针90度 |
| clock_flip | 顺时针90度并垂直翻转 |
旋转方向取决于视频当前的实际躺倒方向,建议先用ffprobe确认rotation值,再选对应transpose参数。实际项目中,如果是用户上传的视频,我建议服务端统一做转正;如果是本地临时文件,则用Flutter侧旋转方案救急。能转码解决的方向问题,尽量不要丢给播放器去猜,这是我处理完一批脏数据之后的真实感受。
6. 横竖屏切换与状态管理:方向适配里最容易被忽略的细节
最后这部分是方向问题的衍生场景:屏幕横竖屏切换、列表复用、加载过渡。用video_player时这些小问题不致命,但都会直接影响体验,我全踩过。
6.1 全屏播放时的屏幕方向锁定:SystemChrome的正确打开方式
进入全屏播放时,很多产品经理的要求是“视频怎么舒服怎么来”——横屏视频自动横屏,竖屏视频保持竖屏。这里需要调用系统方向锁:
import 'package:flutter/services.dart'; // 播放横屏视频进入全屏 Future<void> enterFullscreenLandscape() async { await SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); } // 退出全屏/恢复竖屏 Future<void> exitFullscreen() async { await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); }值得注意的坑是:setPreferredOrientations是异步的,切方向之后UI布局不会立即跟随新方向,所以全屏切换后要等一帧再重建布局。我的做法是切换完后加一个setState回调,或者在下一帧用Future.microtask触发尺寸重读。
还有一个隐藏问题:如果你在竖屏页面进入全屏后锁了横屏,退出全屏时如果忘记恢复竖屏,整个App会一直躺在横屏里,用户只能手动转回来。全屏进出的方向恢复必须成对出现,且放在dispose或兜底逻辑里。一旦中途异常退出,至少还有个保险能恢复portraitUp。
6.2 监听窗口尺寸变化:平板、分屏、折叠屏不迷路
手机竖屏横屏切换只是最基础的情况。到了iPad的Split View、部分手机的分屏、折叠屏开合,窗口尺寸随时在变。这时候如果播放器没有监听窗口变化,容器会跟不上新尺寸,画面被强制拉伸或者黑边满天飞。
用WidgetsBindingObserver监听尺寸变化:
class _PlayerPageState extends State<VideoPlayerPage> with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeMetrics() { super.didChangeMetrics(); final size = MediaQuery.of(context).size; debugPrint('窗口尺寸变化: $size'); setState(() {}); } }didChangeMetrics在键盘弹出、系统UI变化、分屏拖动时都会触发。每次触发后setState一下,让视频容器的AspectRatio重新套用。实测下来,不监听这个,在分屏里视频方向对但比例错,是最难向产品解释的bug——因为截图看画面没变,但用户就是觉得整个播放器“歪着”。
6.3 列表滑动与播放器复用:必须重置方向状态
做信息流时,一个播放器widget往往在多个item之间复用。如果你在某个item上设置了Transform.rotate或自定义旋转角度,切换视频时没有重置,下一个视频会继承上一个的旋转状态,画面方向就是错的。
我的规避方案是:所有方向修正逻辑都绑在具体视频的controller状态上,不挂在widget的公共状态上。切换视频时,重新初始化controller后,要同步重置旋转角度、宽高比、占位比例。简单说,每个视频源用一个独立状态管理,播放器的公共显示组件永远从当前controller的value里取宽高比和方向信息。
void _resetPlayerState() { _angle = 0; _isReady = false; _controller?.removeListener(_listener); _controller?.dispose(); }这行代码看起来平平无奇,但“复用不重置”导致的诡异方向问题,我至少遇到三次。用户反馈“这视频怎么横的”,排查半天发现是上一个视频的旋转状态没清掉。
6.4 加载完成前的“闪烁”处理:占位比例、缓存、首帧展示
前面4.4节说过占位比例问题,这节给个完整一点端的处理思路:
第一层,加载中显示封面图或纯色占位。使用封面图时,封面图尽量用视频同比例截取的帧;拿不到就默认16:9。
第二层,初始化成功后拿到真实宽高比,但先不立刻挂VideoPlayer,而是做一个平滑的尺寸过渡。
第三层,VideoPlayer挂载后,通常还有一小段黑屏等待首帧纹理。这时候在VideoPlayer上层叠一个透明loading遮罩,等controller.value.isInitialized为真且首帧已经渲染后,再撤掉遮罩。
首帧是否已经渲染,video_player没有一个直接回调,我常用做法是初始化后延迟50ms再撤遮罩,实测大部分情况下能覆盖首帧渲染时间。如果追求更稳,可以用controller.addListener监听position变化来判断真正开始播放了。
这一套组合下来,用户首先看到的是封面图,然后尺寸平滑变化成视频真实比例,最后播放器无缝替代封面图。整个过程没有黑屏、没有比例跳动,方向问题的“闪烁感”消弥掉一大半。
最后说几句实在的
折腾完这一整套方向适配之后,我发现解决这个问题的核心思路不是“固定视频方向”,而是“让布局随时准备接受视频的任意宽高比”。视频内容是横是竖,是带旋转标记还是像素已转正,都是数据侧的事实,我们该做的是在每一层都遵循这个事实,渲染层尊重元数据,UI层跟着显示尺寸走,全屏逻辑做好方向恢复。
排查方向问题时,我的习惯是先别改代码,三步走定位:先用系统播放器确认原视频方向对不对,再用ffprobe看一眼像素宽高和rotate标记,最后在Flutter里打一行controller.value.size的日志。三步走完,90%的问题都能立刻定位到是视频源脏数据、播放内核不认旋转标记,还是布局容器没跟对比例。剩下那10%的疑难杂症,再用第六章那种监听和状态重置的思路去追。这套排查方法帮我省下来的时间,保守估计够写两篇这个长度的文章了。