Flutter视频播放插件nexxplay的使用

Flutter视频播放插件nexxplay的使用

特性

Flutter插件作为Android nexxPLAY的包装器,结合示例应用提供的自定义代码,支持以下功能:

  • 可用的nexxPLAY原生视图
  • 动态配置支持
  • 公共API,包括“准备和配置播放器”、“播放控制”、“请求播放器状态和详细信息”等方法
  • 播放器事件观察
  • 全屏模式
  • PiP模式(仅限Android)

请注意,此插件不支持iOS。

集成指南

  1. 添加依赖 首先,将nexxPLAY依赖项添加到pubspec.yaml文件中。

  2. Gradle准备 接下来,需要完成一些Gradle准备工作:

    2.1. 排除META-INFLICENSE文件:

    packagingOptions {
       exclude 'META-INF/ASL2.0'
       exclude 'META-INF/LICENSE'
       exclude 'META-INF/LICENSE.txt'
       exclude 'LICENSE.txt'
       exclude 'META-INF/license.txt'
       exclude 'META-INF/NOTICE'
       exclude 'META-INF/DEPENDENCIES'
       exclude 'META-INF/NOTICE.txt'
       exclude 'META-INF/notice.txt'
    }
    

    参见example/android/app/build.gradle文件以获取更多细节。

    2.2. 禁用混淆。可以通过排除混淆工具(tv.nexx.*)或完全禁用混淆来实现:

    minifyEnabled false
    shrinkResources false
    

    参见example/android/app/build.gradle文件以获取更多细节。

  3. Android原生配置 接下来,需要进行一些Android原生配置:

    3.1. 在Android应用的主清单文件中,向<application>标签添加tools:replace="android:label"。 参见example/android/app/src/main/AndroidManifest.xml文件以获取更多细节。

    3.2. 在Android应用的主清单文件中,为使用的Activity添加android:resizeableActivity="true"android:supportsPictureInPicture="true"。 参见example/android/app/src/main/AndroidManifest.xml文件以获取更多细节。

    3.3. 在Activity的代码中添加或覆盖onUserLeaveHintonPictureInPictureModeChanged方法:

    public class MainActivity extends FlutterActivity {
        // ...
        [@Override](/user/Override)
        public void onUserLeaveHint() {
            super.onUserLeaveHint();
            NexxPlugin.post(OnUserLeaveHintEvent.create());
        }
       
        [@Override](/user/Override)
        public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
            super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
            NexxPlugin.post(OnPIPModeChangedEvent.create(isInPictureInPictureMode,newConfig));
        }
    }
    

    参见example/android/app/src/main/java/tv/nexx/flutter/android_example/MainActivity.java文件以获取更多细节。

    Kotlin版本:

    class MainActivity : FlutterActivity() {
        override fun onUserLeaveHint() {
            super.onUserLeaveHint()
            NexxPlugin.post(OnUserLeaveHintEvent.create())
        }
    
        override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
            super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
            NexxPlugin.post(OnPIPModeChangedEvent.create(isInPictureInPictureMode,newConfig))
        }
    }
    
  4. 继承AppCompat主题 接下来,继承AppCompat主题以满足Android原生应用部分的需求(即使你不使用ChromeCast):

    <style name="NormalTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
        <item name="android:windowBackground">?android:colorBackground</item>
    </style>
    

    确保你有AndroidX AppCompat依赖(直接或间接),例如:

    implementation "androidx.appcompat:appcompat:1.4.0"
    

    参见example/android/app/build.gradleexample/android/app/src/main/res/values/styles.xmlexample/android/app/src/main/res/values-night/styles.xml文件以获取更多细节。

  5. Kotlin Gradle插件(KGP) 使用较新的Android Gradle插件版本时,KGP会包含必要的androidx.*依赖。示例项目使用的是Kotlin 1.9.20和AGP 8.5,Gradle Wrapper版本为8.7。如果遇到类似Fatal Exception: java.lang.RuntimeException: Unable to get provider androidx.startup.InitializationProvider: java.lang.ClassNotFoundException: Didn't find class "androidx.startup.InitializationProvider"的错误提示,则可能需要更新Gradle环境。

  6. 最后一步 最后,从Flutter的角度进行一些操作以确保全屏和PiP支持。示例应用的main.dart文件包含了所有必要的文档。INTEGRATION_GUIDE标记已经放置在文档中,以便于导航。

可选配置

共享MediaSessionCompat实例

7.1. 在build.gradledependencies块中,添加以下行:

implementation "androidx.media:media:1.6.0"

参见example/android/app/build.gradle文件以获取更多细节。

7.2. 在宿主Activity类代码中,重写protected void onCreate([@Nullable](/user/Nullable) Bundle savedInstanceState)方法:

protected void onCreate([@Nullable](/user/Nullable) Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ...其他配置代码
}

参见example/android/app/src/main/java/tv/nexx/flutter/android_example/MainActivity.java文件以获取更多细节。

7.3. 在onCreate(Bundle)方法中,添加以下代码:

final MediaSessionCompat mediaSession = new MediaSessionCompat(getApplicationContext(), getPackageName());
NexxPlayPlugin.addEnvironmentConfigurationEntry(NexxPlayPlugin.KEY_MEDIA_SESSION, mediaSession);

或者,对于Kotlin:

val mediaSession = MediaSessionCompat(applicationContext, packageName)
NexxPlayPlugin.addEnvironmentConfigurationEntry(NexxPlayPlugin.KEY_MEDIA_SESSION, mediaSession)

需要添加以下导入:

import android.support.v4.media.session.MediaSessionCompat;

对于Kotlin版本,省略结尾的分号。

广告支持

8.1. 在build.gradledependencies块中,添加以下行:

implementation "tv.nexx.android:admanager:1.0.05"

参见example/android/app/build.gradle文件以获取更多细节。

8.2. 在宿主Activity类代码中,重写protected void onCreate([@Nullable](/user/Nullable) Bundle savedInstanceState)方法:

protected void onCreate([@Nullable](/user/Nullable) Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ...其他配置代码
}

参见example/android/app/src/main/java/tv/nexx/flutter/android_example/MainActivity.java文件以获取更多细节。

8.3. 在onCreate(Bundle)方法中,添加以下代码:

NexxPlayPlugin.addEnvironmentConfigurationEntry(NexxPlayPlugin.KEY_AD_MANAGER, NexxPlayAdManager::new);

或者,对于Kotlin:

NexxPlayPlugin.addEnvironmentConfigurationEntry(NexxPlayPlugin.KEY_AD_MANAGER, ::NexxPlayAdManager)

需要添加以下导入:

import tv.nexx.android.admanager.NexxPlayAdManager;

对于Kotlin版本,省略结尾的分号。

Chromecast支持

9.1. 确保遵循下一章的原生集成指南中的步骤。

9.2. 应用程序的activity应扩展FlutterFragmentActivity或其子类。

9.3. 当解析CastContext实例时,需要应用一个修改,使CastContext实例包含在插件的配置中:

CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor())
    .addOnSuccessListener(castContext ->  NexxPlayPlugin.addEnvironmentConfigurationEntry(NexxPlayPlugin.KEY_CAST_CONTEXT, castContext))
    .addOnFailureListener(exception ->  Log.e("nexxPLAY", "Could not resolve CastContext", exception));

或者,对于Kotlin:

CastContext.getSharedInstance(this, Executors.newSingleThreadExecutor())
.addOnSuccessListener { NexxPlayPlugin.addEnvironmentConfigurationEntry(NexxPlayPlugin.KEY_CAST_CONTEXT, it) }
.addOnFailureListener { Log.e("nexxPLAY", "Could not resolve CastContext", it) }

需要添加以下导入:

import com.google.android.gms.cast.framework.CastContext;
import java.util.concurrent.Executors;
import android.util.Log;

对于Kotlin版本,省略结尾的分号。

自定义通知图标

10.1. 通过原生钩子设置自定义通知图标。首先,将图像资源添加到Android资源包中。如果是PNG或JPG文件,只需将其放入/android/app/src/main/res/drawable目录即可。 官方文档提供了更多信息。

10.2. 然后在配置中使用图像资源引用。NexxPlayPlugin类包括addNativeConfigurationEntry方法和静态的“key”值KEY_NOTIFICATION_ICON;需要传递的值是图像资源引用——Android框架会自动创建这些变量名(例如,如果你的图像名为“my_image.png”并放在/res/drawable目录下,那么Android将生成R.drawable.my_image变量)。 正常情况下,该配置应在原生Android应用程序的MainActivity类的onCreate方法中:

[@Override](/user/Override)
protected void onCreate([@Nullable](/user/Nullable) Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // ...其他配置代码
    NexxPlayPlugin.addNativeConfigurationEntry(NexxPlayPlugin.KEY_NOTIFICATION_ICON, R.drawable.widget_icon);
}

或者,对于Kotlin:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // ...其他配置代码
    NexxPlayPlugin.addNativeConfigurationEntry(NexxPlayPlugin.KEY_NOTIFICATION_ICON, R.drawable.widget_icon)
}

参见example项目以获取完整的集成示例。

示例代码

以下是example/lib/main.dart文件的部分代码:

import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:nexxplay/nexxplay.dart';

void main() => runApp(const NexxExampleApp());

class NexxExampleApp extends StatelessWidget {
  const NexxExampleApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'nexxPLAY Flutter Testing',
      home: NavigationPage(),
    );
  }
}

class NavigationPage extends StatelessWidget {
  const NavigationPage({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Navigation page')),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.of(context).push<void>(
              MaterialPageRoute(builder: (_) => const _NexxPlayPage()),
            );
          },
          child: const Text('Launch player'),
        ),
      ),
    );
  }
}

class _NexxPlayPage extends StatefulWidget {
  const _NexxPlayPage({Key? key}) : super(key: key);

  [@override](/user/override)
  _NexxPlayPageState createState() => _NexxPlayPageState();
}

class _NexxPlayPageState extends State<_NexxPlayPage> with AdHocVisitor<void> {
  [@override](/user/override)
  Widget build(BuildContext context) => _buildPage();

  Widget _buildPage() {
    return _mode.shouldExpand
        ? _buildFullscreenPlayerPage()
        : _buildNonFullScreenPlayerPage();
  }

  Widget _buildFullscreenPlayerPage() {
    return ScaffoldMessenger(
      key: _messengerKey,
      child: Scaffold(
        body: _buildPlayer(),
      ),
    );
  }

  Widget _buildNonFullScreenPlayerPage() {
    return ScaffoldMessenger(
      key: _messengerKey,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('nexxPLAY example app'),
          actions: _controller == null
              ? []
              : [
                  PopupMenuButton<String>(
                    onSelected: _handleOptionSelection,
                    itemBuilder: (_) => _optionMap.keys
                        .map((o) => PopupMenuItem<String>(value: o, child: Text(o)))
                        .toList(),
                  ),
                ],
        ),
        body: Center(child: _buildContent()),
      ),
    );
  }

  void _handleOptionSelection(String option) => _optionMap[option]?.call(this);

  Widget _buildContent() {
    return Column(
      children: [
        Expanded(child: _buildPlayer()),
        Expanded(child: _buildEventsList()),
      ],
    );
  }

  Widget _buildPlayer() {
    return NexxPlay(
      key: _playerKey,
      environment: _environment,
      configuration: _configuration,
      onControllerCreated: _startPlayer,
    );
  }

  Widget _buildEventsList() {
    return ListView(
      children: _events.reversed
          .map((e) => e.visit(const _WidgetPlayerEventVisitor()))
          .toList(),
    );
  }

  Future<void> _startPlayer(NexxPlayController controller) async {
    try {
      await controller.startPlay(
        playMode: 'video',
        mediaID: '#TODO',
        configuration: _configuration,
      );
      if (!mounted) return;
      _subscribe(controller);
      _controller = controller;
    } on Object catch (e, st) {
      _report('Nexx: exception occurred during player start: \n$e\n$st');
    }
  }

  void _report(String message) {
    debugPrint(message);
    _messengerKey.currentState?.showSnackBar(SnackBar(content: Text(message)));
  }

  void _subscribe(NexxPlayController controller) {
    _subscription = controller.events().listen(
      _consumeEvent,
      onError: (Object e, StackTrace st) {
        _report('Error occurred during events listening: $e');
        debugPrintStack(
          stackTrace: st,
          label: 'Player events error stacktrace',
        );
      },
    );
  }

  void _consumeEvent(PlayerEvent event) {
    event.visit(this);
    _events.add(event);
    if (!_mode.shouldExpand) setState(() {});
  }

  [@override](/user/override)
  void onPlayerEvent(DirectPlayerEvent event) {
    final newMode = _modeTransformation[event.type]?.call(_mode);
    if (newMode != null) setState(() => _mode = newMode);
  }

  [@override](/user/override)
  void dispose() {
    _dispose();
    super.dispose();
  }

  Future<void> _dispose() async {
    await _subscription?.cancel();
    _subscription = null;
    _controller?.dispose();
    _controller = null;
  }

  NexxPlayController? _controller;
  StreamSubscription<PlayerEvent>? _subscription;
  _PlayerMode _mode = const _PlayerMode.initial();
  final List<PlayerEvent> _events = [];
  final _playerKey = GlobalKey<NexxPlayState>();
  final _messengerKey = GlobalKey<ScaffoldMessengerState>();

  final _optionMap = <String, void Function(_NexxPlayPageState)>{
    'Clear Cache': (s) => s._controller?.clearCache(),
    'Play': (s) => s._controller?.play(),
    'Pause': (s) => s._controller?.pause(),
    'Toggle': (s) => s._controller?.toggle(),
    'Mute': (s) => s._controller?.mute(),
    'Unmute': (s) => s._controller?.unmute(),
    'Next': (s) => s._controller?.next(),
    'Previous': (s) => s._controller?.previous(),
    'Seek To 7.5 sec': (s) => s._controller?.seekTo(7.5),
    'Seek By 5 sec': (s) => s._controller?.seekBy(5),
    'Swap to position 1': (s) => s._controller?.swapToPosition(1),
    'Get Current Media': (s) async {
      final data = await s._controller?.getCurrentMedia();
      debugPrint("Current Media: $data");
    },
    'Get Current Media Parent': (s) async {
      final data = await s._controller?.getCurrentMediaParent();
      debugPrint("Get Current Media Parent: $data");
    },
    'Get Audio Tracks': (s) async {
      final data = await s._controller?.getAudioTracks();
      debugPrint("Get Audio Tracks: $data");
    },
    'Get Connected Files': (s) async {
      final data = await s._controller?.getConnectedFiles();
      debugPrint("Get Connected Files: $data");
    },
    'Get Current Playback State': (s) async {
      final data = await s._controller?.getCurrentPlaybackState();
      debugPrint("Current Playback State: $data");
    },
    'Get Current Time': (s) async {
      final data = await s._controller?.getCurrentTime();
      debugPrint("Current Time: $data");
    },
    'Is Playing?': (s) async {
      final data = await s._controller?.isPlaying();
      debugPrint("Is Playing?: $data");
    },
    'Is Playing Ad?': (s) async {
      final data = await s._controller?.isPlayingAd();
      debugPrint("Is Playing Ad?: $data");
    },
    'Is Muted?': (s) async {
      final data = await s._controller?.isMuted();
      debugPrint("Is Muted?: $data");
    },
    'Is In PiP?': (s) async {
      final data = await s._controller?.isInPiP();
      debugPrint("Is In PiP?: $data");
    },
    'Is Casting?': (s) async {
      final data = await s._controller?.isCasting();
      debugPrint("Is Casting?: $data");
    },
    'List Local Media': (s) async {
      // final media = await s._controller?.listLocalMedia('#TODO');
      final media = await s._controller?.listLocalMedia();
      debugPrint(media?.isEmpty ?? true
          ? 'Local Media: No media'
          : 'Local Media: ${media!.join(", ")}');
    },
    'Clear Local Media': (s) {
      // s._controller?.clearLocalMedia('#TODO');
      s._controller?.clearLocalMedia();
    },
    'Disk Space Used For Local Media': (s) async {
      final space = await s._controller?.diskSpaceUsedForLocalMedia();
      debugPrint(space == null ? 'Space Used: Unknown' : 'Space used: $space');
    }
    // 'Update Configuration': (s) {
    //   s._controller?.updateConfiguration(key: '#TODO', value: '#TODO');
    // },
    // 'Update Environment': (s) {
    //   s._controller?.updateEnvironment(key: '#TODO', value: '#TODO');
    // },
    // 'Swap To Media Item': (s) {
    //   s._controller?.swapToMediaItem(
    //       mediaID: '#TODO', streamType: '#TODO', startPosition: 1, delay: 1);
    // },
    // 'Swap To Global ID': (s) {
    //   s._controller
    //       ?.swapToGlobalID(globalID: '#TODO', startPosition: 1, delay: 1);
    // },
    // 'Swap To Remote Media': (s) {
    //   s._controller
    //       ?.swapToRemoteMedia(reference: '#TODO', provider: '#TODO',
    //        delay: 1);
    // },
    // 'Start Downloading Local Media': (s) {
    //   s._controller?.startDownloadingLocalMedia(
    //       mediaID: '#TODO', streamType: '#TODO', provider: '#TODO');
    // },
    // 'Has Download Of Local Media': (s) async {
    //   final bool result = await s._controller?.hasDownloadOfLocalMedia(
    //       mediaID: '#TODO', streamType: '#TODO', provider: '#TODO') ?? false;
    //   debugPrint('Has Download of Local Media? $result');
    // },
  };

  static final _modeTransformation = <NexxEventType, _PlayerMode Function(_PlayerMode)>{
    NexxEventType.enterFullScreen: (mode) => mode.fullscreen(isEnabled: true),
    NexxEventType.exitFullScreen: (mode) => mode.fullscreen(isEnabled: false),
    NexxEventType.enterPIP: (mode) => mode.pip(isEnabled: true),
    NexxEventType.exitPIP: (mode) => mode.pip(isEnabled: false),
  };

  static const _environment = NexxPlayEnvironment({
    'domain': '#TODO',
    'startFullscreen': 0,
  });

  static const _configuration = NexxPlayConfiguration({
    'dataMode': 'API',
    'exitMode': 'load',
    'streamingFilter': '',
    'adType': 'IMA',
    'autoPlay': 0,
    'autoNext': 1,
    'disableAds': 1,
    'hidePrevNext': 0,
    'forcePrevNext': 0,
    'startPosition': 0,
    'delay': 0.0,
  });
}

[@immutable](/user/immutable)
class _PlayerMode {
  final bool isFullscreen;
  final bool isInPIP;

  bool get shouldExpand => isFullscreen || isInPIP;

  const _PlayerMode({required this.isFullscreen, required this.isInPIP});

  const _PlayerMode.initial() : this(isFullscreen: false, isInPIP: false);

  _PlayerMode fullscreen({required bool isEnabled}) =>
      _PlayerMode(isFullscreen: isEnabled, isInPIP: isInPIP);

  _PlayerMode pip({required bool isEnabled}) =>
      _PlayerMode(isFullscreen: isFullscreen, isInPIP: isEnabled);

  [@override](/user/override)
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is _PlayerMode &&
        other.isFullscreen == isFullscreen &&
        other.isInPIP == isInPIP;
  }

  [@override](/user/override)
  int get hashCode => isFullscreen.hashCode ^ isInPIP.hashCode;

  [@override](/user/override)
  String toString() =>
      '_PlayerMode(isFullscreen: $isFullscreen, isInPIP: $isInPIP)';
}

[@immutable](/user/immutable)
class _WidgetPlayerEventVisitor implements PlayerEventVisitor<Widget> {
  const _WidgetPlayerEventVisitor();

  [@override](/user/override)
  Widget onPlayerEvent(DirectPlayerEvent event) {
    return _DirectPlayerEventWidget(event: event);
  }

  [@override](/user/override)
  Widget onPlayerStateChanged(PlayerStateChangeEvent event) {
    return _PlayerStateEventWidget(event: event);
  }
}

class _PlayerStateEventWidget extends StatelessWidget {
  final PlayerStateChangeEvent event;

  const _PlayerStateEventWidget({
    required this.event,
    Key? key,
  }) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return ListTile(
      title: const Text('Player State Change'),
      subtitle: Text('State: ${event.state}'),
    );
  }

  [@override](/user/override)
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('event', event));
  }
}

class _DirectPlayerEventWidget extends StatelessWidget {
  final DirectPlayerEvent event;

  const _DirectPlayerEventWidget({
    required this.event,
    Key? key,
  }) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return ListTile(
      title: const Text('Player Event'),
      subtitle: Text(event.properties.entries
          .map((e) => '${e.key}: ${e.value}')
          .join(', ')),
    );
  }

  [@override](/user/override)
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty('event', event));
  }
}

更多关于Flutter视频播放插件nexxplay的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter视频播放插件nexxplay的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


nexplay 是一个用于 Flutter 应用中的视频播放插件。它提供了丰富的功能,可以帮助你在应用中轻松集成视频播放功能。以下是如何在 Flutter 项目中使用 nexplay 插件的基本步骤。

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 nexplay 插件的依赖。

dependencies:
  flutter:
    sdk: flutter
  nexplay: ^1.0.0  # 请检查最新版本

然后运行 flutter pub get 来获取依赖。

2. 导入插件

在需要使用 nexplay 的 Dart 文件中导入插件:

import 'package:nexplay/nexplay.dart';

3. 创建视频播放器

你可以使用 NexplayPlayer 来创建一个视频播放器。以下是一个简单的示例:

class VideoPlayerScreen extends StatefulWidget {
  @override
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  NexplayPlayerController _controller;

  @override
  void initState() {
    super.initState();
    _controller = NexplayPlayerController(
      videoUrl: 'https://www.example.com/video.mp4', // 视频URL
      playerConfig: NexplayPlayerConfig(
        autoPlay: true, // 是否自动播放
        looping: false, // 是否循环播放
      ),
    );
  }

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Video Player'),
      ),
      body: Center(
        child: NexplayPlayer(
          controller: _controller,
        ),
      ),
    );
  }
}

4. 控制播放器

你可以使用 NexplayPlayerController 来控制视频的播放、暂停、跳转等操作。例如:

_controller.play(); // 播放视频
_controller.pause(); // 暂停视频
_controller.seekTo(Duration(seconds: 10)); // 跳转到指定时间

5. 处理事件

nexplay 插件还提供了事件监听功能,你可以监听视频的播放状态、缓冲状态等:

_controller.onPlayerStateChanged.listen((NexplayPlayerState state) {
  if (state == NexplayPlayerState.PLAYING) {
    print('Video is playing');
  } else if (state == NexplayPlayerState.PAUSED) {
    print('Video is paused');
  }
});

_controller.onBufferingStateChanged.listen((bool isBuffering) {
  if (isBuffering) {
    print('Video is buffering');
  } else {
    print('Video is not buffering');
  }
});

6. 自定义 UI

nexplay 允许你自定义播放器的 UI。你可以通过 NexplayPlayerbuilder 参数来自定义播放器的布局:

NexplayPlayer(
  controller: _controller,
  builder: (BuildContext context, NexplayPlayerController controller) {
    return Container(
      color: Colors.black,
      child: Stack(
        children: [
          controller.videoPlayer,
          Positioned(
            bottom: 10,
            left: 10,
            child: IconButton(
              icon: Icon(
                controller.isPlaying ? Icons.pause : Icons.play_arrow,
                color: Colors.white,
              ),
              onPressed: () {
                if (controller.isPlaying) {
                  controller.pause();
                } else {
                  controller.play();
                }
              },
            ),
          ),
        ],
      ),
    );
  },
);

7. 处理全屏

nexplay 支持全屏播放。你可以通过 NexplayPlayerControllerenterFullScreenexitFullScreen 方法来控制全屏模式:

_controller.enterFullScreen(); // 进入全屏
_controller.exitFullScreen(); // 退出全屏

8. 处理错误

你还可以监听视频播放过程中的错误:

_controller.onError.listen((String error) {
  print('Error occurred: $error');
});
回到顶部