限时福利领取


为什么选择 ExoPlayer?

Flutter 自带的 video_player 插件基于 PlatformView 实现,在复杂场景下容易遇到两个致命问题:

  • Android TextureWidget 内存泄漏:频繁切换视频会导致 SurfaceTexture 无法释放,引发 OOM
  • iOS 卡顿明显:PlatformView 与 Flutter 引擎的同步机制导致 60FPS 视频掉帧率达 30%

video_player 与 ExoPlayer 性能对比

技术方案对比

| 方案 | 首帧加载(ms) | 内存占用(MB) | 支持格式 | |---------------|-------------|-------------|----------------| | video_player | 1200 | 85 | 基础 MP4/HLS | | chewie | 1100 | 90 | 带UI控制的MP4 | | ExoPlayer | 700 | 55 | DASH/RTMP/FLV |

核心实现步骤

1. 原生层封装(Android)

  1. build.gradle 添加依赖:

    implementation 'com.google.android.exoplayer:exoplayer:2.18.1'
  2. 创建 ExoPlayerHolder 单例管理播放器实例:

    class ExoPlayerHolder(context: Context) {
        private val player = ExoPlayer.Builder(context)
            .setLoadControl(
                DefaultLoadControl.Builder()
                    .setBufferDurationsMs(
                        **30000**, // minBufferMs
                        **60000**, // maxBufferMs
                        **1000**,  // playbackBufferMs
                        **2000**   // playbackRebufferMs
                    ).build()
            ).build()
    
        fun release() {
            player.release()
        }
    }

2. Dart 层状态管理

使用 Riverpod 实现播放状态同步:

final playerProvider = StateNotifierProvider<VideoStateNotifier, VideoState>((ref) {
  return VideoStateNotifier();
});

class VideoState {
  final bool isPlaying;
  final Duration position;
  // 其他状态字段...
}

class VideoStateNotifier extends StateNotifier<VideoState> {
  VideoStateNotifier() : super(VideoState.initial());

  void updatePosition(Duration pos) {
    state = state.copyWith(position: pos);
  }
}

3. 关键性能参数

  • 缓冲公式maxBufferMs = 平均码率(bps) × 目标缓冲时长(s) / 8
  • Android 8.0 内存泄漏解决方案
    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            textureView.setSurfaceTextureListener(null);
        }
    }

避坑实践

  • iOS 硬解码触发条件
  • 必须使用 .h264 编码
  • 分辨率 ≤ 1920×1080
  • 系统版本 ≥ iOS 11

  • 多实例内存回收

    @override
    void dispose() {
      _channel.invokeMethod('releasePlayer');
      super.dispose();  // 必须最后调用
    }

性能数据(Redmi K40)

| 指标 | 优化前 | 优化后 | |--------------|-------|-------| | 首帧加载 | 1.2s | 0.7s | | 内存占用 | 85MB | 55MB | | 60FPS 保持率 | 70% | 98% |

性能优化对比图

延伸思考

对于弱网环境,可以扩展实现 HLS 自适应码率策略:

  1. 在 ExoPlayer 中配置带宽计量器
  2. 根据 networkType 动态切换 AdaptiveTrackSelection
  3. Dart 层监听网络变化事件:
    final connectivity = Connectivity();
    connectivity.onConnectivityChanged.listen((result) {
      if (result == ConnectivityResult.mobile) {
        _channel.invokeMethod('setLowBitrate');
      }
    });

实际项目中还需要考虑 CDN 切换、预加载策略等优化点,这些我们后续可以继续探讨。

Logo

音视频技术社区,一个全球开发者共同探讨、分享、学习音视频技术的平台,加入我们,与全球开发者一起创造更加优秀的音视频产品!

更多推荐