公告

👇欢迎大家关注公众号&私信交流👇

Skip to content

Flutter 与音频视频播放桥接

YGKing NaN-aN-aNFlutter音频播放视频播放媒体控制后台播放播放器 4936 个字 18 分钟

故事开始:小刘的音乐视频应用

小刘正在开发一个音乐视频播放应用,需要支持本地和在线媒体的播放、播放列表管理、后台播放等功能。他发现 Flutter 中的媒体播放比想象中要复杂。

"音频视频播放涉及格式兼容性、播放控制、后台模式、音频焦点等多个方面,而且 Android 和 iOS 的实现差异很大。"小刘在开发笔记中写道。

第一章:媒体播放技术基础

1.1 音频播放技术概述

现代移动设备的音频播放功能非常丰富:

音频格式支持:

  • MP3:最广泛使用的音频格式
  • AAC:高质量压缩格式,iOS 首选
  • FLAC:无损音频格式
  • OGG:开源音频格式
  • WAV:无损音频格式,文件较大
  • M4A:Apple 音频容器格式

音频功能:

  • 播放、暂停、停止
  • 快进、快退
  • 音量控制
  • 播放速度调节
  • 均衡器
  • 后台播放
  • 音频会话管理

1.2 视频播放技术概述

视频播放技术相对复杂:

视频格式支持:

  • MP4:最广泛使用的视频格式
  • MOV:Apple 视频格式
  • AVI:传统视频格式
  • MKV:开源视频容器
  • WebM:Web 优化视频格式
  • HLS:HTTP Live Streaming

视频功能:

  • 播放、暂停、停止
  • 快进、快退
  • 全屏播放
  • 播放速度调节
  • 字幕支持
  • 画质切换
  • 画中画模式

1.3 Flutter 媒体播放开发生态

Flutter 中媒体播放开发主要有以下几种方案:

  1. video_player - 官方推荐的视频播放插件
  2. just_audio - 功能强大的音频播放插件
  3. audioplayers - 简单易用的音频播放插件
  4. 自定义平台通道 - 完全自定义实现

第二章:环境搭建与基础配置

2.1 添加依赖

yaml
dependencies:
  flutter:
    sdk: flutter
  video_player: ^2.8.1
  just_audio: ^0.9.36
  audio_session: ^0.1.16
  rxdart: ^0.27.7
  permission_handler: ^11.0.1

2.2 权限配置

Android 权限配置(android/app/src/main/AndroidManifest.xml)

xml
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- 存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                 android:maxSdkVersion="28" />

<!-- Android 13+ 需要这些权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

<!-- 后台播放权限 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<!-- 音频焦点 -->
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

iOS 权限配置(ios/Runner/Info.plist)

xml
<!-- 后台播放权限 -->
<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

<!-- 网络权限 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

2.3 权限管理实现

dart
import 'package:permission_handler/permission_handler.dart';

class MediaPermissionManager {
  static Future<bool> requestAudioPermissions() async {
    if (Platform.isAndroid) {
      final androidInfo = await DeviceInfoPlugin().androidInfo;
      final sdkInt = androidInfo.version.sdkInt;

      if (sdkInt >= 33) {
        // Android 13+
        final audio = await Permission.audio.request();
        return audio.isGranted;
      } else {
        // Android 12及以下
        final storage = await Permission.storage.request();
        return storage.isGranted;
      }
    } else {
      // iOS不需要特殊权限
      return true;
    }
  }

  static Future<bool> requestVideoPermissions() async {
    if (Platform.isAndroid) {
      final androidInfo = await DeviceInfoPlugin().androidInfo;
      final sdkInt = androidInfo.version.sdkInt;

      if (sdkInt >= 33) {
        // Android 13+
        final video = await Permission.videos.request();
        return video.isGranted;
      } else {
        // Android 12及以下
        final storage = await Permission.storage.request();
        return storage.isGranted;
      }
    } else {
      // iOS不需要特殊权限
      return true;
    }
  }

  static Future<bool> checkAudioPermissions() async {
    if (Platform.isAndroid) {
      final androidInfo = await DeviceInfoPlugin().androidInfo;
      final sdkInt = androidInfo.version.sdkInt;

      if (sdkInt >= 33) {
        return await Permission.audio.isGranted;
      } else {
        return await Permission.storage.isGranted;
      }
    } else {
      return true;
    }
  }

  static Future<bool> checkVideoPermissions() async {
    if (Platform.isAndroid) {
      final androidInfo = await DeviceInfoPlugin().androidInfo;
      final sdkInt = androidInfo.version.sdkInt;

      if (sdkInt >= 33) {
        return await Permission.videos.isGranted;
      } else {
        return await Permission.storage.isGranted;
      }
    } else {
      return true;
    }
  }

  static Future<void> openSettings() async {
    await openAppSettings();
  }
}

第三章:音频播放功能实现

3.1 音频播放管理器

dart
import 'package:just_audio/just_audio.dart';
import 'package:audio_session/audio_session.dart';

class AudioPlayerManager {
  static final AudioPlayerManager _instance = AudioPlayerManager._internal();
  factory AudioPlayerManager() => _instance;
  AudioPlayerManager._internal();

  final AudioPlayer _player = AudioPlayer();
  final List<AudioSource> _playlist = [];
  int _currentIndex = 0;
  bool _isInitialized = false;

  AudioPlayer get player => _player;
  List<AudioSource> get playlist => List.unmodifiable(_playlist);
  int get currentIndex => _currentIndex;
  bool get isInitialized => _isInitialized;

  Stream<PlayerState> get playerStateStream => _player.playerStateStream;
  Stream<Duration?> get positionStream => _player.positionStream;
  Stream<Duration?> get durationStream => _player.durationStream;
  Stream<SequenceState?> get sequenceStateStream => _player.sequenceStateStream;

  Future<void> initialize() async {
    try {
      // 配置音频会话
      final session = await AudioSession.instance;
      await session.configure(const AudioSessionConfiguration.speech());

      // 设置播放器属性
      await _player.setLoopMode(LoopMode.off);
      await _player.setShuffleModeEnabled(false);

      _isInitialized = true;
    } catch (e) {
      throw AudioException('音频播放器初始化失败:${e.toString()}');
    }
  }

  Future<void> loadPlaylist(List<AudioSource> sources) async {
    try {
      _playlist.clear();
      _playlist.addAll(sources);
      _currentIndex = 0;

      await _player.setAudioSource(
        ConcatenatingAudioSource(children: _playlist),
        initialIndex: _currentIndex,
      );
    } catch (e) {
      throw AudioException('加载播放列表失败:${e.toString()}');
    }
  }

  Future<void> loadSingleAudio(AudioSource source) async {
    try {
      _playlist.clear();
      _playlist.add(source);
      _currentIndex = 0;

      await _player.setAudioSource(source);
    } catch (e) {
      throw AudioException('加载音频失败:${e.toString()}');
    }
  }

  Future<void> play() async {
    try {
      await _player.play();
    } catch (e) {
      throw AudioException('播放失败:${e.toString()}');
    }
  }

  Future<void> pause() async {
    try {
      await _player.pause();
    } catch (e) {
      throw AudioException('暂停失败:${e.toString()}');
    }
  }

  Future<void> stop() async {
    try {
      await _player.stop();
    } catch (e) {
      throw AudioException('停止失败:${e.toString()}');
    }
  }

  Future<void> seek(Duration position) async {
    try {
      await _player.seek(position);
    } catch (e) {
      throw AudioException('跳转失败:${e.toString()}');
    }
  }

  Future<void> next() async {
    try {
      await _player.seekToNext();
      _currentIndex = (_currentIndex + 1) % _playlist.length;
    } catch (e) {
      throw AudioException('下一首失败:${e.toString()}');
    }
  }

  Future<void> previous() async {
    try {
      await _player.seekToPrevious();
      _currentIndex = (_currentIndex - 1 + _playlist.length) % _playlist.length;
    } catch (e) {
      throw AudioException('上一首失败:${e.toString()}');
    }
  }

  Future<void> setVolume(double volume) async {
    try {
      await _player.setVolume(volume.clamp(0.0, 1.0));
    } catch (e) {
      throw AudioException('设置音量失败:${e.toString()}');
    }
  }

  Future<void> setSpeed(double speed) async {
    try {
      await _player.setSpeed(speed.clamp(0.5, 2.0));
    } catch (e) {
      throw AudioException('设置播放速度失败:${e.toString()}');
    }
  }

  Future<void> setLoopMode(LoopMode mode) async {
    try {
      await _player.setLoopMode(mode);
    } catch (e) {
      throw AudioException('设置循环模式失败:${e.toString()}');
    }
  }

  Future<void> setShuffleModeEnabled(bool enabled) async {
    try {
      await _player.setShuffleModeEnabled(enabled);
    } catch (e) {
      throw AudioException('设置随机播放失败:${e.toString()}');
    }
  }

  Future<void> addToPlaylist(AudioSource source) async {
    try {
      _playlist.add(source);
      await _player.setAudioSource(
        ConcatenatingAudioSource(children: _playlist),
        initialIndex: _currentIndex,
      );
    } catch (e) {
      throw AudioException('添加到播放列表失败:${e.toString()}');
    }
  }

  Future<void> removeFromPlaylist(int index) async {
    try {
      if (index < 0 || index >= _playlist.length) {
        throw AudioException('索引超出范围');
      }

      _playlist.removeAt(index);

      if (index < _currentIndex) {
        _currentIndex--;
      } else if (index == _currentIndex && _currentIndex >= _playlist.length) {
        _currentIndex = _playlist.length - 1;
      }

      await _player.setAudioSource(
        ConcatenatingAudioSource(children: _playlist),
        initialIndex: _currentIndex,
      );
    } catch (e) {
      throw AudioException('从播放列表移除失败:${e.toString()}');
    }
  }

  Future<void> clearPlaylist() async {
    try {
      _playlist.clear();
      _currentIndex = 0;
      await _player.setAudioSource(null);
    } catch (e) {
      throw AudioException('清空播放列表失败:${e.toString()}');
    }
  }

  Future<void> playAtIndex(int index) async {
    try {
      if (index < 0 || index >= _playlist.length) {
        throw AudioException('索引超出范围');
      }

      _currentIndex = index;
      await _player.seek(Duration.zero, index: index);
      await _player.play();
    } catch (e) {
      throw AudioException('播放指定索引失败:${e.toString()}');
    }
  }

  AudioSource? getCurrentAudio() {
    if (_currentIndex >= 0 && _currentIndex < _playlist.length) {
      return _playlist[_currentIndex];
    }
    return null;
  }

  Future<AudioMetadata?> getCurrentMetadata() async {
    final currentAudio = getCurrentAudio();
    if (currentAudio == null) return null;

    try {
      final sequenceState = await _player.sequenceState.first;
      final currentSource = sequenceState?.currentSource;

      if (currentSource != null) {
        return AudioMetadata(
          title: currentSource.tag.title ?? '未知标题',
          artist: currentSource.tag.artist ?? '未知艺术家',
          album: currentSource.tag.album ?? '未知专辑',
          duration: currentSource.tag.duration ?? Duration.zero,
          artwork: currentSource.tag.artwork,
        );
      }

      return null;
    } catch (e) {
      return null;
    }
  }

  Future<void> dispose() async {
    try {
      await _player.dispose();
      _isInitialized = false;
    } catch (e) {
      throw AudioException('释放播放器失败:${e.toString()}');
    }
  }
}

class AudioException implements Exception {
  final String message;
  final String? code;

  AudioException(this.message, {this.code});

  @override
  String toString() {
    return 'AudioException: $message${code != null ? ' (Code: $code)' : ''}';
  }
}

class AudioMetadata {
  final String title;
  final String artist;
  final String album;
  final Duration duration;
  final Uint8List? artwork;

  AudioMetadata({
    required this.title,
    required this.artist,
    required this.album,
    required this.duration,
    this.artwork,
  });

  String get formattedDuration {
    final minutes = duration.inMinutes;
    final seconds = duration.inSeconds % 60;
    return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  }
}

3.2 音频播放 UI 组件

dart
class AudioPlayerPage extends StatefulWidget {
  @override
  _AudioPlayerPageState createState() => _AudioPlayerPageState();
}

class _AudioPlayerPageState extends State<AudioPlayerPage> {
  final AudioPlayerManager _audioManager = AudioPlayerManager();
  PlayerState? _playerState;
  Duration? _position;
  Duration? _duration;
  AudioMetadata? _metadata;
  double _volume = 1.0;
  double _speed = 1.0;
  LoopMode _loopMode = LoopMode.off;
  bool _isShuffleModeEnabled = false;

  @override
  void initState() {
    super.initState();
    _initializeAudioPlayer();
  }

  @override
  void dispose() {
    _audioManager.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('音频播放器'),
        actions: [
          IconButton(
            icon: Icon(Icons.playlist_play),
            onPressed: _showPlaylist,
          ),
        ],
      ),
      body: SingleChildScrollView(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            _buildAlbumArt(),
            SizedBox(height: 24),
            _buildTrackInfo(),
            SizedBox(height: 24),
            _buildProgressBar(),
            SizedBox(height: 24),
            _buildPlaybackControls(),
            SizedBox(height: 24),
            _buildAdvancedControls(),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _loadSampleAudio,
        child: Icon(Icons.add),
      ),
    );
  }

  Widget _buildAlbumArt() {
    return Container(
      width: 200,
      height: 200,
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(12),
        color: Colors.grey[300],
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.2),
            blurRadius: 10,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: _metadata?.artwork != null
          ? ClipRRect(
              borderRadius: BorderRadius.circular(12),
              child: Image.memory(
                _metadata!.artwork!,
                fit: BoxFit.cover,
              ),
            )
          : Icon(
              Icons.music_note,
              size: 80,
              color: Colors.grey[600],
            ),
    );
  }

  Widget _buildTrackInfo() {
    return Column(
      children: [
        Text(
          _metadata?.title ?? '未加载音频',
          style: TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.bold,
          ),
          textAlign: TextAlign.center,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
        ),
        SizedBox(height: 8),
        Text(
          '${_metadata?.artist ?? '未知艺术家'}${_metadata?.album ?? '未知专辑'}',
          style: TextStyle(
            fontSize: 16,
            color: Colors.grey[600],
          ),
          textAlign: TextAlign.center,
          maxLines: 2,
          overflow: TextOverflow.ellipsis,
        ),
      ],
    );
  }

  Widget _buildProgressBar() {
    return Column(
      children: [
        SliderTheme(
          data: SliderTheme.of(context).copyWith(
            thumbShape: RoundSliderThumbShape(enabledThumbRadius: 8),
            trackHeight: 4,
          ),
          child: Slider(
            value: _position?.inMilliseconds.toDouble() ?? 0.0,
            max: _duration?.inMilliseconds.toDouble() ?? 0.0,
            onChanged: (value) {
              _audioManager.seek(Duration(milliseconds: value.toInt()));
            },
          ),
        ),
        SizedBox(height: 8),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(_formatDuration(_position)),
            Text(_formatDuration(_duration)),
          ],
        ),
      ],
    );
  }

  Widget _buildPlaybackControls() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        IconButton(
          icon: Icon(Icons.shuffle),
          onPressed: _toggleShuffleMode,
          color: _isShuffleModeEnabled ? Theme.of(context).primaryColor : null,
        ),
        IconButton(
          icon: Icon(Icons.skip_previous),
          onPressed: _audioManager.previous,
          iconSize: 40,
        ),
        Container(
          decoration: BoxDecoration(
            shape: BoxShape.circle,
            color: Theme.of(context).primaryColor,
          ),
          child: IconButton(
            icon: Icon(
              _playerState?.playing == true ? Icons.pause : Icons.play_arrow,
              color: Colors.white,
            ),
            onPressed: _togglePlayback,
            iconSize: 40,
          ),
        ),
        IconButton(
          icon: Icon(Icons.skip_next),
          onPressed: _audioManager.next,
          iconSize: 40,
        ),
        IconButton(
          icon: Icon(_getLoopModeIcon()),
          onPressed: _toggleLoopMode,
          color: _loopMode != LoopMode.off ? Theme.of(context).primaryColor : null,
        ),
      ],
    );
  }

  Widget _buildAdvancedControls() {
    return Column(
      children: [
        Row(
          children: [
            Icon(Icons.volume_up),
            SizedBox(width: 12),
            Expanded(
              child: Slider(
                value: _volume,
                onChanged: (value) {
                  setState(() {
                    _volume = value;
                  });
                  _audioManager.setVolume(value);
                },
              ),
            ),
            Text('${(_volume * 100).toInt()}%'),
          ],
        ),
        SizedBox(height: 16),
        Row(
          children: [
            Icon(Icons.speed,
            SizedBox(width: 12),
            Expanded(
              child: Slider(
                value: _speed,
                min: 0.5,
                max: 2.0,
                divisions: 3,
                onChanged: (value) {
                  setState(() {
                    _speed = value;
                  });
                  _audioManager.setSpeed(value);
                },
              ),
            ),
            Text('${_speed}x'),
          ],
        ),
      ],
    );
  }

  IconData _getLoopModeIcon() {
    switch (_loopMode) {
      case LoopMode.off:
        return Icons.loop;
      case LoopMode.one:
        return Icons.repeat_one;
      case LoopMode.all:
        return Icons.repeat;
    }
  }

  String _formatDuration(Duration? duration) {
    if (duration == null) return '00:00';

    final minutes = duration.inMinutes;
    final seconds = duration.inSeconds % 60;
    return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  }

  Future<void> _initializeAudioPlayer() async {
    try {
      await _audioManager.initialize();

      // 监听播放状态
      _audioManager.playerStateStream.listen((state) {
        setState(() {
          _playerState = state;
        });
      });

      // 监听播放位置
      _audioManager.positionStream.listen((position) {
        setState(() {
          _position = position;
        });
      });

      // 监听音频时长
      _audioManager.durationStream.listen((duration) {
        setState(() {
          _duration = duration;
        });
      });

      // 监听当前播放的音频
      _audioManager.sequenceStateStream.listen((_) async {
        final metadata = await _audioManager.getCurrentMetadata();
        setState(() {
          _metadata = metadata;
        });
      });
    } catch (e) {
      _showErrorDialog('初始化失败', e.toString());
    }
  }

  void _togglePlayback() async {
    try {
      if (_playerState?.playing == true) {
        await _audioManager.pause();
      } else {
        await _audioManager.play();
      }
    } catch (e) {
      _showErrorDialog('播放失败', e.toString());
    }
  }

  void _toggleLoopMode() async {
    LoopMode nextMode;
    switch (_loopMode) {
      case LoopMode.off:
        nextMode = LoopMode.one;
        break;
      case LoopMode.one:
        nextMode = LoopMode.all;
        break;
      case LoopMode.all:
        nextMode = LoopMode.off;
        break;
    }

    try {
      await _audioManager.setLoopMode(nextMode);
      setState(() {
        _loopMode = nextMode;
      });
    } catch (e) {
      _showErrorDialog('设置循环模式失败', e.toString());
    }
  }

  void _toggleShuffleMode() async {
    try {
      final enabled = !_isShuffleModeEnabled;
      await _audioManager.setShuffleModeEnabled(enabled);
      setState(() {
        _isShuffleModeEnabled = enabled;
      });
    } catch (e) {
      _showErrorDialog('设置随机播放失败', e.toString());
    }
  }

  Future<void> _loadSampleAudio() async {
    try {
      final hasPermission = await MediaPermissionManager.checkAudioPermissions();
      if (!hasPermission) {
        final granted = await MediaPermissionManager.requestAudioPermissions();
        if (!granted) {
          _showPermissionDialog();
          return;
        }
      }

      // 加载示例音频
      final audioSource = AudioSource.uri(
        Uri.parse('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'),
        tag: MediaItem(
          id: 'sample_1',
          title: '示例音频',
          artist: 'SoundHelix',
          album: '示例专辑',
        ),
      );

      await _audioManager.loadSingleAudio(audioSource);
    } catch (e) {
      _showErrorDialog('加载音频失败', e.toString());
    }
  }

  void _showPlaylist() {
    showModalBottomSheet(
      context: context,
      builder: (context) => PlaylistPage(audioManager: _audioManager),
    );
  }

  void _showErrorDialog(String title, String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('确定'),
          ),
        ],
      ),
    );
  }

  void _showPermissionDialog() {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('需要权限'),
        content: Text('应用需要音频权限来播放媒体文件'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              MediaPermissionManager.openSettings();
            },
            child: Text('去设置'),
          ),
        ],
      ),
    );
  }
}

第四章:视频播放功能实现

4.1 视频播放管理器

dart
import 'package:video_player/video_player.dart';

class VideoPlayerManager {
  VideoPlayerController? _controller;
  bool _isInitialized = false;
  bool _isPlaying = false;
  Duration _position = Duration.zero;
  Duration _duration = Duration.zero;
  double _volume = 1.0;
  double _playbackSpeed = 1.0;

  VideoPlayerController? get controller => _controller;
  bool get isInitialized => _isInitialized;
  bool get isPlaying => _isPlaying;
  Duration get position => _position;
  Duration get duration => _duration;
  double get volume => _volume;
  double get playbackSpeed => _playbackSpeed;

  Future<void> initialize(String dataSource) async {
    try {
      _controller = VideoPlayerController.network(dataSource);

      await _controller!.initialize();

      // 监听播放状态
      _controller!.addListener(_onControllerUpdate);

      _isInitialized = true;
    } catch (e) {
      throw VideoException('视频播放器初始化失败:${e.toString()}');
    }
  }

  Future<void> initializeFromFile(File file) async {
    try {
      _controller = VideoPlayerController.file(file);

      await _controller!.initialize();

      _controller!.addListener(_onControllerUpdate);

      _isInitialized = true;
    } catch (e) {
      throw VideoException('视频播放器初始化失败:${e.toString()}');
    }
  }

  void _onControllerUpdate() {
    if (_controller == null) return;

    setState(() {
      _isPlaying = _controller!.value.isPlaying;
      _position = _controller!.value.position;
      _duration = _controller!.value.duration ?? Duration.zero;
      _volume = _controller!.value.volume;
      _playbackSpeed = _controller!.value.playbackSpeed;
    });
  }

  void setState(VoidCallback fn) {
    fn();
    // 这里可以添加状态更新通知
  }

  Future<void> play() async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.play();
    } catch (e) {
      throw VideoException('播放失败:${e.toString()}');
    }
  }

  Future<void> pause() async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.pause();
    } catch (e) {
      throw VideoException('暂停失败:${e.toString()}');
    }
  }

  Future<void> stop() async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.pause();
      await _controller!.seekTo(Duration.zero);
    } catch (e) {
      throw VideoException('停止失败:${e.toString()}');
    }
  }

  Future<void> seekTo(Duration position) async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.seekTo(position);
    } catch (e) {
      throw VideoException('跳转失败:${e.toString()}');
    }
  }

  Future<void> setVolume(double volume) async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.setVolume(volume.clamp(0.0, 1.0));
    } catch (e) {
      throw VideoException('设置音量失败:${e.toString()}');
    }
  }

  Future<void> setPlaybackSpeed(double speed) async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.setPlaybackSpeed(speed.clamp(0.25, 2.0));
    } catch (e) {
      throw VideoException('设置播放速度失败:${e.toString()}');
    }
  }

  Future<void> setLooping(bool looping) async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.setLooping(looping);
    } catch (e) {
      throw VideoException('设置循环播放失败:${e.toString()}');
    }
  }

  Future<void> enterFullscreen() async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.enterFullscreen();
    } catch (e) {
      throw VideoException('进入全屏失败:${e.toString()}');
    }
  }

  Future<void> exitFullscreen() async {
    if (_controller == null || !_isInitialized) {
      throw VideoException('播放器未初始化');
    }

    try {
      await _controller!.exitFullscreen();
    } catch (e) {
      throw VideoException('退出全屏失败:${e.toString()}');
    }
  }

  Future<void> dispose() async {
    try {
      await _controller?.dispose();
      _controller = null;
      _isInitialized = false;
    } catch (e) {
      throw VideoException('释放播放器失败:${e.toString()}');
    }
  }

  String get formattedPosition {
    return _formatDuration(_position);
  }

  String get formattedDuration {
    return _formatDuration(_duration);
  }

  double get progress {
    if (_duration.inMilliseconds == 0) return 0.0;
    return _position.inMilliseconds / _duration.inMilliseconds;
  }

  String _formatDuration(Duration duration) {
    final hours = duration.inHours;
    final minutes = duration.inMinutes % 60;
    final seconds = duration.inSeconds % 60;

    if (hours > 0) {
      return '${hours.toString().padLeft(2, '0')}:'
             '${minutes.toString().padLeft(2, '0')}:'
             '${seconds.toString().padLeft(2, '0')}';
    } else {
      return '${minutes.toString().padLeft(2, '0')}:'
             '${seconds.toString().padLeft(2, '0')}';
    }
  }
}

class VideoException implements Exception {
  final String message;
  final String? code;

  VideoException(this.message, {this.code});

  @override
  String toString() {
    return 'VideoException: $message${code != null ? ' (Code: $code)' : ''}';
  }
}

4.2 视频播放 UI 组件

dart
class VideoPlayerPage extends StatefulWidget {
  final String? videoUrl;
  final File? videoFile;

  const VideoPlayerPage({
    this.videoUrl,
    this.videoFile,
  });

  @override
  _VideoPlayerPageState createState() => _VideoPlayerPageState();
}

class _VideoPlayerPageState extends State<VideoPlayerPage> {
  final VideoPlayerManager _videoManager = VideoPlayerManager();
  bool _showControls = true;
  Timer? _hideControlsTimer;

  @override
  void initState() {
    super.initState();
    _initializeVideoPlayer();
  }

  @override
  void dispose() {
    _hideControlsTimer?.cancel();
    _videoManager.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: GestureDetector(
        onTap: _toggleControls,
        child: Stack(
          children: [
            // 视频播放器
            Center(
              child: _buildVideoPlayer(),
            ),

            // 控制面板
            if (_showControls)
              Positioned(
                top: 0,
                left: 0,
                right: 0,
                bottom: 0,
                child: Container(
                  decoration: BoxDecoration(
                    gradient: LinearGradient(
                      begin: Alignment.topCenter,
                      end: Alignment.bottomCenter,
                      colors: [
                        Colors.black.withOpacity(0.7),
                        Colors.transparent,
                        Colors.transparent,
                        Colors.black.withOpacity(0.7),
                      ],
                      stops: [0.0, 0.2, 0.8, 1.0],
                    ),
                  ),
                  child: Column(
                    children: [
                      _buildTopControls(),
                      Spacer(),
                      _buildBottomControls(),
                    ],
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildVideoPlayer() {
    if (!_videoManager.isInitialized) {
      return Container(
        width: double.infinity,
        height: double.infinity,
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              CircularProgressIndicator(color: Colors.white),
              SizedBox(height: 16),
              Text(
                '正在加载视频...',
                style: TextStyle(color: Colors.white),
              ),
            ],
          ),
        ),
      );
    }

    return AspectRatio(
      aspectRatio: _videoManager.controller!.value.aspectRatio,
      child: VideoPlayer(_videoManager.controller!),
    );
  }

  Widget _buildTopControls() {
    return SafeArea(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Row(
          children: [
            IconButton(
              icon: Icon(Icons.arrow_back, color: Colors.white),
              onPressed: () => Navigator.pop(context),
            ),
            Spacer(),
            IconButton(
              icon: Icon(Icons.fullscreen, color: Colors.white),
              onPressed: _toggleFullscreen,
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildBottomControls() {
    return SafeArea(
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          children: [
            // 进度条
            Row(
              children: [
                Text(
                  _videoManager.formattedPosition,
                  style: TextStyle(color: Colors.white, fontSize: 12),
                ),
                SizedBox(width: 12),
                Expanded(
                  child: SliderTheme(
                    data: SliderTheme.of(context).copyWith(
                      thumbShape: RoundSliderThumbShape(enabledThumbRadius: 6),
                      trackHeight: 3,
                      thumbColor: Colors.white,
                      activeTrackColor: Colors.white,
                      inactiveTrackColor: Colors.white.withOpacity(0.3),
                    ),
                    child: Slider(
                      value: _videoManager.progress,
                      onChanged: (value) {
                        final position = Duration(
                          milliseconds: (value * _videoManager.duration.inMilliseconds).toInt(),
                        );
                        _videoManager.seekTo(position);
                      },
                    ),
                  ),
                ),
                SizedBox(width: 12),
                Text(
                  _videoManager.formattedDuration,
                  style: TextStyle(color: Colors.white, fontSize: 12),
                ),
              ],
            ),
            SizedBox(height: 16),
            // 播放控制
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                IconButton(
                  icon: Icon(Icons.replay_10, color: Colors.white),
                  onPressed: _rewind10Seconds,
                ),
                IconButton(
                  icon: Icon(
                    _videoManager.isPlaying ? Icons.pause : Icons.play_arrow,
                    color: Colors.white,
                    size: 40,
                  ),
                  onPressed: _togglePlayback,
                ),
                IconButton(
                  icon: Icon(Icons.forward_10, color: Colors.white),
                  onPressed: _forward10Seconds,
                ),
              ],
            ),
            SizedBox(height: 16),
            // 高级控制
            Row(
              children: [
                Icon(Icons.volume_up, color: Colors.white, size: 20),
                SizedBox(width: 8),
                Expanded(
                  child: SliderTheme(
                    data: SliderTheme.of(context).copyWith(
                      thumbShape: RoundSliderThumbShape(enabledThumbRadius: 4),
                      trackHeight: 2,
                      thumbColor: Colors.white,
                      activeTrackColor: Colors.white,
                      inactiveTrackColor: Colors.white.withOpacity(0.3),
                    ),
                    child: Slider(
                      value: _videoManager.volume,
                      onChanged: (value) {
                        _videoManager.setVolume(value);
                      },
                    ),
                  ),
                ),
                SizedBox(width: 8),
                PopupMenuButton<String>(
                  icon: Icon(Icons.speed, color: Colors.white),
                  onSelected: _setPlaybackSpeed,
                  itemBuilder: (context) => [
                    PopupMenuItem(
                      value: '0.5',
                      child: Text('0.5x'),
                    ),
                    PopupMenuItem(
                      value: '0.75',
                      child: Text('0.75x'),
                    ),
                    PopupMenuItem(
                      value: '1.0',
                      child: Text('1.0x'),
                    ),
                    PopupMenuItem(
                      value: '1.25',
                      child: Text('1.25x'),
                    ),
                    PopupMenuItem(
                      value: '1.5',
                      child: Text('1.5x'),
                    ),
                    PopupMenuItem(
                      value: '2.0',
                      child: Text('2.0x'),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _initializeVideoPlayer() async {
    try {
      if (widget.videoUrl != null) {
        await _videoManager.initialize(widget.videoUrl!);
      } else if (widget.videoFile != null) {
        await _videoManager.initializeFromFile(widget.videoFile!);
      } else {
        throw VideoException('未提供视频源');
      }

      setState(() {});

      // 开始播放
      await _videoManager.play();
    } catch (e) {
      _showErrorDialog('初始化失败', e.toString());
    }
  }

  void _toggleControls() {
    setState(() {
      _showControls = !_showControls;
    });

    if (_showControls) {
      _hideControlsTimer?.cancel();
      _hideControlsTimer = Timer(Duration(seconds: 3), () {
        if (mounted) {
          setState(() {
            _showControls = false;
          });
        }
      });
    }
  }

  void _togglePlayback() async {
    try {
      if (_videoManager.isPlaying) {
        await _videoManager.pause();
      } else {
        await _videoManager.play();
      }
    } catch (e) {
      _showErrorDialog('播放失败', e.toString());
    }
  }

  void _rewind10Seconds() async {
    final newPosition = _videoManager.position - Duration(seconds: 10);
    final clampedPosition = newPosition < Duration.zero
        ? Duration.zero
        : newPosition;
    await _videoManager.seekTo(clampedPosition);
  }

  void _forward10Seconds() async {
    final newPosition = _videoManager.position + Duration(seconds: 10);
    final clampedPosition = newPosition > _videoManager.duration
        ? _videoManager.duration
        : newPosition;
    await _videoManager.seekTo(clampedPosition);
  }

  void _setPlaybackSpeed(String speed) async {
    await _videoManager.setPlaybackSpeed(double.parse(speed));
  }

  Future<void> _toggleFullscreen() async {
    try {
      if (_videoManager.controller?.value.isFullScreen == true) {
        await _videoManager.exitFullscreen();
      } else {
        await _videoManager.enterFullscreen();
      }
    } catch (e) {
      _showErrorDialog('全屏切换失败', e.toString());
    }
  }

  void _showErrorDialog(String title, String message) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('确定'),
          ),
        ],
      ),
    );
  }
}

第五章:后台播放和音频焦点

5.1 后台播放服务

dart
import 'package:audio_service/audio_service.dart';

class BackgroundAudioService {
  static Future<void> initialize() async {
    final audioServiceHandler = AudioServiceHandler();

    await AudioService.init(
      builder: () => audioServiceHandler,
      config: AudioServiceConfig(
        androidNotificationChannelId: 'com.example.audio_service.audio',
        androidNotificationChannelName: 'Audio Service',
        androidNotificationOngoing: true,
        androidStopForegroundOnPause: true,
        androidNotificationIcon: 'mipmap/ic_launcher',
        androidEnableQueue: true,
        androidNotificationColor: 0xFF2196F3,
        notificationColor: Color(0xFF2196F3),
        artDownscaleWidth: 600,
        artDownscaleHeight: 600,
      ),
    );
  }
}

class AudioServiceHandler extends BaseAudioHandler with QueueHandler {
  final AudioPlayerManager _audioManager = AudioPlayerManager();

  AudioServiceHandler() {
    _listenToPlaybackEvents();
  }

  @override
  Future<void> play() async {
    if (queue.value.isNotEmpty) {
      final mediaItem = queue.value[mediaItem.value?.index ?? 0];
      await _audioManager.playAtIndex(mediaItem.index ?? 0);
      mediaItem.add(mediaItem);
    }
  }

  @override
  Future<void> pause() async {
    await _audioManager.pause();
  }

  @override
  Future<void> stop() async {
    await _audioManager.stop();
    await super.stop();
  }

  @override
  Future<void> seek(Duration position) async {
    await _audioManager.seek(position);
  }

  @override
  Future<void> skipToNext() async {
    await _audioManager.next();
    final currentIndex = _audioManager.currentIndex;
    if (currentIndex < queue.value.length) {
      mediaItem.add(queue.value[currentIndex]);
    }
  }

  @override
  Future<void> skipToPrevious() async {
    await _audioManager.previous();
    final currentIndex = _audioManager.currentIndex;
    if (currentIndex < queue.value.length) {
      mediaItem.add(queue.value[currentIndex]);
    }
  }

  @override
  Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
    LoopMode loopMode;
    switch (repeatMode) {
      case AudioServiceRepeatMode.none:
        loopMode = LoopMode.off;
        break;
      case AudioServiceRepeatMode.one:
        loopMode = LoopMode.one;
        break;
      case AudioServiceRepeatMode.all:
      case AudioServiceRepeatMode.group:
        loopMode = LoopMode.all;
        break;
    }

    await _audioManager.setLoopMode(loopMode);
    repeatMode.add(repeatMode);
  }

  @override
  Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
    final enabled = shuffleMode == AudioServiceShuffleMode.all;
    await _audioManager.setShuffleModeEnabled(enabled);
    shuffleMode.add(shuffleMode);
  }

  void _listenToPlaybackEvents() {
    _audioManager.playerStateStream.listen((state) {
      playbackState.add(_mapPlayerState(state));
    });

    _audioManager.positionStream.listen((position) {
      playbackState.add(playbackState.value.copyWith(
        updatePosition: position,
      ));
    });

    _audioManager.sequenceStateStream.listen((sequenceState) async {
      if (sequenceState?.currentSource != null) {
        final metadata = await _audioManager.getCurrentMetadata();
        if (metadata != null) {
          final mediaItem = MediaItem(
            id: sequenceState!.currentIndex.toString(),
            title: metadata.title,
            artist: metadata.artist,
            album: metadata.album,
            duration: metadata.duration,
            artUri: metadata.artwork != null
                ? Uri.parse('data:image/jpeg;base64,${base64Encode(metadata.artwork!)}')
                : null,
          );

          queue.add([mediaItem]);
          mediaItem.add(mediaItem);
        }
      }
    });
  }

  PlaybackState _mapPlayerState(PlayerState playerState) {
    return PlaybackState(
      controls: [
        MediaControl.skipToPrevious,
        MediaControl.playPause,
        MediaControl.skipToNext,
      ],
      systemActions: const {
        MediaAction.seek,
        MediaAction.seekForward,
        MediaAction.seekBackward,
      },
      androidCompactActionIndices: const [0, 1, 2],
      processingState: const {
        ProcessingState.idle: AudioProcessingState.idle,
        ProcessingState.loading: AudioProcessingState.loading,
        ProcessingState.buffering: AudioProcessingState.buffering,
        ProcessingState.ready: AudioProcessingState.ready,
        ProcessingState.completed: AudioProcessingState.completed,
      }[playerState.processingState] ?? AudioProcessingState.idle,
      playing: playerState.playing,
      updatePosition: playerState.position,
      bufferedPosition: playerState.bufferedPosition,
      speed: playerState.speed,
      updateTime: DateTime.now(),
    );
  }
}

5.2 音频焦点管理

dart
import 'package:audio_session/audio_session.dart';

class AudioFocusManager {
  static AudioSession? _session;
  static bool _hasFocus = false;

  static Future<void> initialize() async {
    try {
      _session = await AudioSession.instance;
      await _session!.configure(const AudioSessionConfiguration.music());

      // 监听音频焦点变化
      _session!.interruptionEventStream.listen(_handleInterruption);

      // 监听音频路由变化
      _session!.becomingNoisyEventStream.listen(_handleBecomingNoisy);
    } catch (e) {
      throw AudioException('音频焦点初始化失败:${e.toString()}');
    }
  }

  static Future<void> requestFocus() async {
    try {
      if (_session != null) {
        await _session!.setActive(true);
        _hasFocus = true;
      }
    } catch (e) {
      throw AudioException('请求音频焦点失败:${e.toString()}');
    }
  }

  static Future<void> releaseFocus() async {
    try {
      if (_session != null) {
        await _session!.setActive(false);
        _hasFocus = false;
      }
    } catch (e) {
      throw AudioException('释放音频焦点失败:${e.toString()}');
    }
  }

  static bool get hasFocus => _hasFocus;

  static void _handleInterruption(InterruptionEvent event) {
    switch (event.begin) {
      case true:
        // 音频被中断
        _hasFocus = false;
        if (event.primary) {
          // 其他应用开始播放音频
          // 暂停当前播放
          AudioPlayerManager().pause();
        }
        break;
      case false:
        // 中断结束
        if (event.primary) {
          // 其他应用停止播放音频
          // 请求音频焦点并恢复播放
          requestFocus().then((_) {
            AudioPlayerManager().play();
          });
        }
        break;
    }
  }

  static void _handleBecomingNoisy() {
    // 耳机拔出等事件
    AudioPlayerManager().pause();
  }
}

故事结局:小刘的成功

经过几个月的开发,小刘的音乐视频播放应用终于完成了!用户可以流畅地播放音频和视频,支持后台播放、播放列表管理等功能。

"媒体播放功能是音视频应用的核心,通过合理的架构设计和用户体验优化,我们打造出了专业的播放体验。"小刘在项目总结中写道,"特别是后台播放和音频焦点管理,确保了应用在复杂场景下的稳定性。"

小刘的应用获得了用户的好评,特别是流畅的播放体验和丰富的控制功能。他的成功证明了:掌握音频视频播放桥接技术,是开发媒体类应用的关键技能。

总结

通过小刘的音乐视频播放应用开发故事,我们全面学习了 Flutter 音频视频播放桥接技术:

核心技术

  • 音频播放:本地和在线音频播放、播放控制
  • 视频播放:本地和在线视频播放、全屏支持
  • 播放控制:播放、暂停、快进、快退、音量控制
  • 播放列表:列表管理、顺序播放、随机播放

高级特性

  • 后台播放:音频服务、通知控制
  • 音频焦点:焦点管理、中断处理
  • 播放速度:变速播放、音调保持
  • 循环模式:单曲循环、列表循环

最佳实践

  • 用户体验:流畅的界面和及时反馈
  • 性能优化:内存管理和资源释放
  • 错误处理:异常捕获和用户提示
  • 平台适配:Android 和 iOS 的差异处理

音频视频播放桥接技术为 Flutter 应用打开了多媒体世界的大门,让开发者能够构建出丰富的媒体应用。掌握这些技术,将帮助你在移动应用开发中创造更多可能!