flutter开发实战-ijkplayer视频播放器功能

使用better_player播放器进行播放视频时候,在Android上会出现解码失败的问题,better_player使用的是video_player,video_player很多视频无法解码。最终采用ijkplayer播放器插件,在flutter上使用fijkplayer插件。
在这里插入图片描述
在这里插入图片描述

一、引入fijkplayer

在使用fijkplayer前可以先看下https://fijkplayer.befovy.com/docs/zh/fijkplayer-api.html

在工程的pubspec.yaml中引入插件

  fijkplayer: ^0.11.0
    

fijkPlayer 就是对 native C 层 ijkplayer 的一个 dart 包装,接口都保持一致。 FijkPlayer 处理所有播放相关的工作,实际工作都是由 native C 层 ijkplayer 完成,包含检查 dataSource 中的媒体信息,打开解码器和解码线程、打开音频输出设备、将解码后数据输出给音频设备或显示设备。

二、使用fijkplayer

2.1、IJKVideoPlayerController控制常用操作

使用fijkplayer,这里创建了IJKVideoPlayer来嵌套一下FijkView,使用IJKVideoPlayerController来控制常用功能操作

IJKVideoPlayerController如下

import 'dart:async';

class IJKVideoPlayerController {
  FutureOr Function()? stop;
  FutureOr Function()? pause;
  FutureOr Function()? play;
  FutureOr Function(int msec)? seekTo;
  FutureOr Function(double volume)? setVolume;
  FutureOr Function(double speed)? setSpeed;
  FutureOr Function(int loopCount)? setLoop;
  FutureOr Function()? isPlaying;
}
    

IJKVideoPlayerController来控制停止、暂停、播放、seek、设置音量、设置播放速率、设置循环次数、获取是否在播放中等。

  • 播放视频
  void play() {
    if (videoPlayerController.play != null) {
      videoPlayerController.play!.call();
    }
  }
    
  • 暂停视频播放
  void pause() {
    if (videoPlayerController.pause != null) {
      videoPlayerController.pause!.call();
    }
  }
    
  • 停止视频播放
  void stop() {
    if (videoPlayerController.stop != null) {
      videoPlayerController.stop!.call();
    }
  }
    
  • seek指定位置
  void seekTo(int msec) {
    if (videoPlayerController.seekTo != null) {
      videoPlayerController.seekTo!.call(msec);
    }
  }
    
  • 设置音量
  void setVolume(double volume) {
    if (videoPlayerController.setVolume != null) {
      videoPlayerController.setVolume!.call(volume);
    }
  }
    
  • 设置播放速率
  void setSpeed(double speed) {
    if (videoPlayerController.setSpeed != null) {
      videoPlayerController.setSpeed!.call(speed);
    }
  }
    
  • 设置循环次数
  void setLoop(int loopCount) {
    if (videoPlayerController.setLoop != null) {
      videoPlayerController.setLoop!.call(loopCount);
    }
  }
    
  • 获取是否播放中
  Future<bool?> isPlaying() async {
    if (videoPlayerController.isPlaying != null) {
      bool videoIsPlaying = await videoPlayerController.isPlaying!.call();
      return videoIsPlaying;
    }
    return Future.value(null);
  }
    

2.2、在ijkplayer设置source,使用FijkPlayer

在设置播放器的时候,需要设置source类型。fijkplayer提供了两种方式,一种是本地工程文件、一种是网络视频地址。

  • 设置网络视频源
  /// usage
  /// autoPlay 为 true 时等同于连续调用 setDataSource、prepareAsync、start
  fplayer.setDataSource("http://samplevideo.com/sample.flv", autoPlay: true);
    
  • 设置本地资源作为播放源
  /// pubspec.yml 中需要指定assets 内容
  ///   assets:
  ///     - assets/butterfly.mp4
  ///
  /// scheme 是 `asset`, `://` 是 scheme 分隔符, `/` 是路径起始符号
  fplayer.setDataSource("asset:///assets/butterfly.mp4", autoPlay: true);
    

在setDataSource还有autoPlay(自动播放),showCover(是否显示视频封面,视频默认获取第一帧作为视频封面)

2.3、FijkView显示视频的控件Widget

在fijkplayer中,使用FijkView来显示视频。

  FijkView({
    required this.player,
    this.width,
    this.height,
    this.fit = FijkFit.contain,
    this.fsFit = FijkFit.contain,
    this.panelBuilder = defaultFijkPanelBuilder,
    this.color = const Color(0xFF607D8B),
    this.cover,
    this.fs = true,
    this.onDispose,
  });
    

可以设置显示fit、全屏的fit、背景颜色color、封面图(设置之后会显示在视频播放的上面)、是否全屏等。

在这里我们如果需要自定义样式,可以替换掉panelBuilder。

2.4、自定义控件IJKVideoPanel

在这里我们如果需要自定义样式,可以替换掉panelBuilder。我们自定义一个IJKVideoPanel,这个大部分代码来源default,这里调整了部分样式。

IJKVideoPanel完整代码如下

import 'dart:async';
import 'dart:math';

import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/material.dart';

class IJKVideoPanel extends StatefulWidget {
  const IJKVideoPanel({
    super.key,
    required this.player,
    required this.buildContext,
    required this.viewSize,
    required this.texturePos,
  });

  final FijkPlayer player;
  final BuildContext buildContext;
  final Size viewSize;
  final Rect texturePos;

  @override
  State<IJKVideoPanel> createState() => _IJKVideoPanelState();
}

class _IJKVideoPanelState extends State<IJKVideoPanel> {
  FijkPlayer get player => widget.player;

  Duration _duration = Duration();
  Duration _currentPos = Duration();
  Duration _bufferPos = Duration();

  bool _playing = false;
  bool _prepared = false;
  String? _exception;

  // bool _buffering = false;

  double _seekPos = -1.0;

  StreamSubscription? _currentPosSubs;

  StreamSubscription? _bufferPosSubs;

  //StreamSubscription _bufferingSubs;

  Timer? _hideTimer;
  bool _hideStuff = true;

  double _volume = 1.0;

  final barHeight = 40.0;

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

    _duration = player.value.duration;
    _currentPos = player.currentPos;
    _bufferPos = player.bufferPos;
    _prepared = player.state.index >= FijkState.prepared.index;
    _playing = player.state == FijkState.started;
    _exception = player.value.exception.message;
    // _buffering = player.isBuffering;

    player.addListener(_playerValueChanged);

    _currentPosSubs = player.onCurrentPosUpdate.listen((v) {
      setState(() {
        _currentPos = v;
      });
    });

    _bufferPosSubs = player.onBufferPosUpdate.listen((v) {
      setState(() {
        _bufferPos = v;
      });
    });
  }

  void _playerValueChanged() {
    FijkValue value = player.value;
    if (value.duration != _duration) {
      setState(() {
        _duration = value.duration;
      });
    }

    bool playing = (value.state == FijkState.started);
    bool prepared = value.prepared;
    String? exception = value.exception.message;
    if (playing != _playing ||
        prepared != _prepared ||
        exception != _exception) {
      setState(() {
        _playing = playing;
        _prepared = prepared;
        _exception = exception;
      });
    }
  }

  void _playOrPause() {
    if (_playing == true) {
      player.pause();
    } else {
      player.start();
    }
  }

  @override
  void dispose() {
    super.dispose();
    _hideTimer?.cancel();

    player.removeListener(_playerValueChanged);
    _currentPosSubs?.cancel();
    _bufferPosSubs?.cancel();
  }

  void _startHideTimer() {
    _hideTimer?.cancel();
    _hideTimer = Timer(const Duration(seconds: 3), () {
      setState(() {
        _hideStuff = true;
      });
    });
  }

  void _cancelAndRestartTimer() {
    if (_hideStuff == true) {
      _startHideTimer();
    }
    setState(() {
      _hideStuff = !_hideStuff;
    });
  }

  Widget _buildVolumeButton() {
    IconData iconData;
    if (_volume <= 0) {
      iconData = Icons.volume_off;
    } else {
      iconData = Icons.volume_up;
    }
    return IconButton(
      icon: Icon(iconData, color: Colors.white),
      padding: EdgeInsets.only(left: 10.0, right: 10.0),
      onPressed: () {
        setState(() {
          _volume = _volume > 0 ? 0.0 : 1.0;
          player.setVolume(_volume);
        });
      },
    );
  }

  AnimatedOpacity _buildBottomBar(BuildContext context) {
    double duration = _duration.inMilliseconds.toDouble();
    double currentValue =
        _seekPos > 0 ? _seekPos : _currentPos.inMilliseconds.toDouble();
    currentValue = min(currentValue, duration);
    currentValue = max(currentValue, 0);
    return AnimatedOpacity(
      opacity: _hideStuff ? 0.0 : 0.8,
      duration: Duration(milliseconds: 400),
      child: Container(
        height: barHeight,
        decoration: BoxDecoration(
          gradient: LinearGradient(
            colors: [Colors.transparent, Colors.black45],
            begin: Alignment.topCenter,
            end: Alignment.bottomCenter,
          ),
        ),
        child: Row(
          children: <Widget>[
            _buildVolumeButton(),
            Padding(
              padding: EdgeInsets.only(right: 5.0, left: 5),
              child: Text(
                '${_duration2String(_currentPos)}',
                style: TextStyle(fontSize: 14.0, color: Colors.white),
              ),
            ),

            _duration.inMilliseconds == 0
                ? Expanded(child: Center())
                : Expanded(
                    child: Padding(
                      padding: EdgeInsets.only(right: 0, left: 0),
                      child: FijkSlider(
                        value: currentValue,
                        cacheValue: _bufferPos.inMilliseconds.toDouble(),
                        min: 0.0,
                        max: duration,
                        onChanged: (v) {
                          _startHideTimer();
                          setState(() {
                            _seekPos = v;
                          });
                        },
                        onChangeEnd: (v) {
                          setState(() {
                            player.seekTo(v.toInt());
                            print("seek to $v");
                            _currentPos =
                                Duration(milliseconds: _seekPos.toInt());
                            _seekPos = -1;
                          });
                        },
                      ),
                    ),
                  ),

            // duration / position
            _duration.inMilliseconds == 0
                ? Container(child: const Text("LIVE"))
                : Padding(
                    padding: EdgeInsets.only(right: 5.0, left: 5),
                    child: Text(
                      '${_duration2String(_duration)}',
                      style: TextStyle(fontSize: 14.0, color: Colors.white),
                    ),
                  ),

//             IconButton(
//               icon: Icon(widget.player.value.fullScreen
//                   ? Icons.fullscreen_exit
//                   : Icons.fullscreen),
//               padding: EdgeInsets.only(left: 10.0, right: 10.0),
// //              color: Colors.transparent,
//               onPressed: () {
//                 widget.player.value.fullScreen
//                     ? player.exitFullScreen()
//                     : player.enterFullScreen();
//               },
//             )
            //
          ],
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    // Rect rect = player.value.fullScreen
    //     ? Rect.fromLTWH(0, 0, widget.viewSize.width, widget.viewSize.height)
    //     : Rect.fromLTRB(
    //     max(0.0, widget.texturePos.left),
    //     max(0.0, widget.texturePos.top),
    //     min(widget.viewSize.width, widget.texturePos.right),
    //     min(widget.viewSize.height, widget.texturePos.bottom));
    Rect rect =
        Rect.fromLTWH(0, 0, widget.viewSize.width, widget.viewSize.height);

    return Positioned.fromRect(
      rect: rect,
      child: GestureDetector(
        onTap: _cancelAndRestartTimer,
        child: AbsorbPointer(
          absorbing: _hideStuff,
          child: Column(
            children: <Widget>[
              Container(height: barHeight),
              Expanded(
                child: GestureDetector(
                  onTap: () {
                    _cancelAndRestartTimer();
                  },
                  child: Container(
                    color: Colors.transparent,
                    height: double.infinity,
                    width: double.infinity,
                    child: Center(
                        child: _exception != null
                            ? Text(
                                _exception!,
                                style: TextStyle(
                                  color: Colors.white,
                                  fontSize: 25,
                                ),
                              )
                            : (_prepared ||
                                    player.state == FijkState.initialized)
                                ? AnimatedOpacity(
                                    opacity: _hideStuff ? 0.0 : 0.85,
                                    duration: Duration(milliseconds: 400),
                                    child: IconButton(
                                        iconSize: barHeight * 2,
                                        icon: Icon(
                                          _playing
                                              ? Icons.pause
                                              : Icons.play_arrow,
                                          color: Colors.white,
                                          size: 44,
                                        ),
                                        padding: EdgeInsets.only(
                                            left: 10.0, right: 10.0),
                                        onPressed: _playOrPause))
                                : SizedBox(
                                    width: barHeight * 1.5,
                                    height: barHeight * 1.5,
                                    child: CircularProgressIndicator(
                                        valueColor: AlwaysStoppedAnimation(
                                            Colors.white)),
                                  )),
                  ),
                ),
              ),
              _buildBottomBar(context),
            ],
          ),
        ),
      ),
    );
  }
}

String _duration2String(Duration duration) {
  if (duration.inMilliseconds < 0) return "-: negtive";

  String twoDigits(int n) {
    if (n >= 10) return "$n";
    return "0$n";
  }

  String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
  String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
  int inHours = duration.inHours;
  return inHours > 0
      ? "$inHours:$twoDigitMinutes:$twoDigitSeconds"
      : "$twoDigitMinutes:$twoDigitSeconds";
}

    

2.5、嵌套FijkView的IJKVideoPlayer

在使用时候,使用了IJKVideoPlayer来封装了一层FijkView。
在IJKVideoPlayer中设置了videoPlayerController控制播放的操作 如停止、暂停、播放、seek、设置音量、设置播放速率、设置循环次数、获取是否在播放中。

IJKVideoPlayer完整代码如下

  import 'package:fijkplayer/fijkplayer.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_demolab/ijk_player/ijk_video_panel.dart';

import 'ijk_video_player_controller.dart';

/// usage
/// autoPlay 为 true 时等同于连续调用 setDataSource、prepareAsync、start
/// fplayer.setDataSource("http://samplevideo.com/sample.flv", autoPlay: true);
///
/// 设置本地资源作为播放源,
/// pubspec.yml 中需要指定assets 内容
///   assets:
///     - assets/butterfly.mp4
///
/// scheme 是 `asset`, `://` 是 scheme 分隔符, `/` 是路径起始符号
/// fplayer.setDataSource("asset:///assets/butterfly.mp4", autoPlay: true);

class IJKVideoPlayer extends StatefulWidget {
  const IJKVideoPlayer({
    super.key,
    required this.path,
    this.autoPlay = false,
    this.showCover = true,
    this.fit = FijkFit.contain,
    this.cover,
    this.color = Colors.black,
    this.width,
    this.height,
    this.videoPlayerController,
  });

  final double? width;
  final double? height;

  final String path;
  final bool autoPlay;
  final bool showCover;
  final FijkFit fit;
  final Widget? cover;
  final Color color;
  final IJKVideoPlayerController? videoPlayerController;

  @override
  State<IJKVideoPlayer> createState() => _IJKVideoPlayerState();
}

class _IJKVideoPlayerState extends State<IJKVideoPlayer> {
  final FijkPlayer player = FijkPlayer();

  @override
  void initState() {
    super.initState();
    player.setDataSource(
      widget.path,
      autoPlay: widget.autoPlay,
      showCover: widget.showCover,
    );
    addVideoPlayerFun();
  }

  void addVideoPlayerFun() {
    if (widget.videoPlayerController != null) {
      widget.videoPlayerController!.play = () {
        // 触发播放
        player.start();
      };

      widget.videoPlayerController!.stop = () {
        // 触发停止
        player.stop();
      };

      widget.videoPlayerController!.pause = () {
        // 触发暂停
        player.pause();
      };

      widget.videoPlayerController!.setLoop = (int loopCount) {
        // 触发setLoop
        if (loopCount < 0) {
          loopCount = 1;
        }
        player.setLoop(loopCount);
      };

      widget.videoPlayerController!.seekTo = (int msec) {
        // 触发seek
        if (msec < 0) {
          msec = 0;
        }
        player.seekTo(msec);
      };

      widget.videoPlayerController!.setVolume = (double volume) {
        // 触发setVolume
        if (volume < 0.0) {
          volume = 0.0;
        }

        player.setVolume(volume);
      };

      widget.videoPlayerController!.setSpeed = (double speed) {
        // 触发setSpeed
        if (speed < 0.0) {
          speed = 1.0;
        }

        player.setSpeed(speed);
      };

      widget.videoPlayerController!.isPlaying = () {
        // 触发setVolume
        if (FijkState.started == player.state) {
          return true;
        } else {
          return false;
        }
      };
    }
  }

  @override
  void dispose() {
    super.dispose();
    player.release();
  }

  void onIJKDispose(FijkData fijkData) {}

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment: Alignment.center,
      child: Stack(
        alignment: Alignment.center,
        children: [
          widget.cover != null ? widget.cover! : Container(),
          FijkView(
            width: widget.width,
            height: widget.height,
            player: player,
            fit: widget.fit,
            fsFit: widget.fit,
            color: widget.color,
            onDispose: onIJKDispose,
            panelBuilder: (FijkPlayer player, FijkData data,
                BuildContext context, Size viewSize, Rect texturePos) {
              return IJKVideoPanel(
                player: player,
                buildContext: context,
                viewSize: viewSize,
                texturePos: texturePos,
              );
            },
          ),
        ],
      ),
    );
  }
}

    

三、最后使用IJKVideoPlayer的IJKVideoPage页面

这里我创建了一个IJKVideoPage来使用IJKVideoPlayer视频播放,IJKVideoPlayer中需要path与videoPlayerController

IJKVideoPage完整代码如下

  import 'dart:async';

import 'package:flutter/material.dart';

import 'ijk_player/ijk_video_player.dart';
import 'ijk_player/ijk_video_player_controller.dart';

class IJKVideoPage extends StatefulWidget {
  const IJKVideoPage({
    super.key,
    required this.url,
  });

  final String url;

  @override
  State<IJKVideoPage> createState() => _IJKVideoPageState();
}

class _IJKVideoPageState extends State<IJKVideoPage> {
  final IJKVideoPlayerController videoPlayerController =
      IJKVideoPlayerController();

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

  @override
  Widget build(BuildContext context) {
    Size size = MediaQuery.of(context).size;
    return Scaffold(
      appBar: AppBar(title: Text("Fijkplayer Example")),
      body: Center(
        child: Container(
          width: size.width,
          height: size.width * 9.0 / 16.0,
          alignment: Alignment.center,
          child: IJKVideoPlayer(
            path: widget.url,
            videoPlayerController: videoPlayerController,
            color: Colors.black,
          ),
        ),
      ),
    );
  }

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

    

如果外面的页面跳转到播放页面,需要设置url

  void testIJKVideoPage(BuildContext context) {
    Navigator.of(context)
        .push(MaterialPageRoute(builder: (BuildContext context) {
      return IJKVideoPage(
          url: "https://vd2.bdstatic.com/mda-maif0tt1rirqp27q/540p/h264_cae/1611052585/mda-maif0tt1rirqp27q.mp4");
    }));
  }
    

https://brucegwo.blog.csdn.net/article/details/136024588

四、小结

flutter开发实战-ijkplayer视频播放器功能

学习记录,每天不停进步。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐