Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Flutterで動画を再生する

 Flutterで動画を再生する

Kentaro Kawakami

February 24, 2021
Tweet

More Decks by Kentaro Kawakami

Other Decks in Programming

Transcript

  1. ύοέʔδͷ঺հ • video_player • pub.dev • GitHub • flutter͕ग़͍ͯ͠Δ •

    iOS/Android/WebʹରԠ • iOS͸Objective-CɺAndroid͸Java
  2. ϩʔΧϧϑΝΠϧɾΞηοτͷ࠶ੜ class LocalVideo extends StatefulWidget { @override _LocalVideoState createState() =>

    _LocalVideoState(); } class _LocalVideoState extends State<LocalVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); // _controller = VideoPlayerController.file(File('file path')); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  3. ϩʔΧϧϑΝΠϧɾΞηοτͷ࠶ੜ class LocalVideo extends StatefulWidget { @override _LocalVideoState createState() =>

    _LocalVideoState(); } class _LocalVideoState extends State<LocalVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); // _controller = VideoPlayerController.file(File('file path')); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  4. ϩʔΧϧϑΝΠϧɾΞηοτͷ࠶ੜ class LocalVideo extends StatefulWidget { @override _LocalVideoState createState() =>

    _LocalVideoState(); } class _LocalVideoState extends State<LocalVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); // _controller = VideoPlayerController.file(File('file path')); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  5. ϩʔΧϧϑΝΠϧɾΞηοτͷ࠶ੜ class LocalVideo extends StatefulWidget { @override _LocalVideoState createState() =>

    _LocalVideoState(); } class _LocalVideoState extends State<LocalVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); // _controller = VideoPlayerController.file(File('file path')); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  6. ϩʔΧϧϑΝΠϧɾΞηοτͷ࠶ੜ class LocalVideo extends StatefulWidget { @override _LocalVideoState createState() =>

    _LocalVideoState(); } class _LocalVideoState extends State<LocalVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); // _controller = VideoPlayerController.file(File('file path')); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  7. ϩʔΧϧϑΝΠϧɾΞηοτͷ࠶ੜ class LocalVideo extends StatefulWidget { @override _LocalVideoState createState() =>

    _LocalVideoState(); } class _LocalVideoState extends State<LocalVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); // _controller = VideoPlayerController.file(File('file path')); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  8. ϦϞʔτϑΝΠϧͷ࠶ੜ class RemoteVideo extends StatefulWidget { @override _RemoteVideoState createState() =>

    _RemoteVideoState(); } class _RemoteVideoState extends State<RemoteVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  9. ϦϞʔτϑΝΠϧͷ࠶ੜ class RemoteVideo extends StatefulWidget { @override _RemoteVideoState createState() =>

    _RemoteVideoState(); } class _RemoteVideoState extends State<RemoteVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  10. ϦϞʔτϑΝΠϧͷ࠶ੜ class RemoteVideo extends StatefulWidget { @override _RemoteVideoState createState() =>

    _RemoteVideoState(); } class _RemoteVideoState extends State<RemoteVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.network('https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8'); _controller.initialize(); _controller.play(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return … VideoPlayer(_controller); … } }
  11. ࣈນ class RemoteVideo extends StatefulWidget { … } class _RemoteVideoState

    extends State<RemoteVideo> { late VideoPlayerController _controller; Future<ClosedCaptionFile> _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context).loadString('assets/bumble_bee_captions.srt'); return SubRipCaptionFile(fileContents); } @override void initState() { super.initState(); _controller = VideoPlayerController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', closedCaptionFile: _loadCaptions(), ); _controller.initialize(); _controller.play(); } … @override Widget build(BuildContext context) { return … Stack( children: <Widget>[ VideoPlayer(_controller), ClosedCaption(text: _controller.value.caption.text), ], ); … } }
  12. ࣈນ class RemoteVideo extends StatefulWidget { … } class _RemoteVideoState

    extends State<RemoteVideo> { late VideoPlayerController _controller; Future<ClosedCaptionFile> _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context).loadString('assets/bumble_bee_captions.srt'); return SubRipCaptionFile(fileContents); } @override void initState() { super.initState(); _controller = VideoPlayerController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', closedCaptionFile: _loadCaptions(), ); _controller.initialize(); _controller.play(); } … @override Widget build(BuildContext context) { return … Stack( children: <Widget>[ VideoPlayer(_controller), ClosedCaption(text: _controller.value.caption.text), ], ); … } }
  13. ࣈນ class RemoteVideo extends StatefulWidget { … } class _RemoteVideoState

    extends State<RemoteVideo> { late VideoPlayerController _controller; Future<ClosedCaptionFile> _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context).loadString('assets/bumble_bee_captions.srt'); return SubRipCaptionFile(fileContents); } @override void initState() { super.initState(); _controller = VideoPlayerController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', closedCaptionFile: _loadCaptions(), ); _controller.initialize(); _controller.play(); } … @override Widget build(BuildContext context) { return … Stack( children: <Widget>[ VideoPlayer(_controller), ClosedCaption(text: _controller.value.caption.text), ], ); … } }
  14. ࣈນ class RemoteVideo extends StatefulWidget { … } class _RemoteVideoState

    extends State<RemoteVideo> { late VideoPlayerController _controller; Future<ClosedCaptionFile> _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context).loadString('assets/bumble_bee_captions.srt'); return SubRipCaptionFile(fileContents); } @override void initState() { super.initState(); _controller = VideoPlayerController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', closedCaptionFile: _loadCaptions(), ); _controller.initialize(); _controller.play(); } … @override Widget build(BuildContext context) { return … Stack( children: <Widget>[ VideoPlayer(_controller), ClosedCaption(text: _controller.value.caption.text), ], ); … } }
  15. γʔΫόʔ class RemoteVideo extends StatefulWidget { … } class _RemoteVideoState

    extends State<RemoteVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'); _controller.initialize(); _controller.play(); } … @override Widget build(BuildContext context) { return … Stack( alignment: Alignment.bottomCenter, children: <Widget>[ VideoPlayer(_controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ); … } }
  16. γʔΫόʔ class RemoteVideo extends StatefulWidget { … } class _RemoteVideoState

    extends State<RemoteVideo> { late VideoPlayerController _controller; @override void initState() { super.initState(); _controller = VideoPlayerController.network('https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'); _controller.initialize(); _controller.play(); } … @override Widget build(BuildContext context) { return … Stack( alignment: Alignment.bottomCenter, children: <Widget>[ VideoPlayer(_controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ); … } }
  17. ϥΠϑαΠΫϧʹ߹Θͤͯ࠶ੜʗఀࢭ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { _VideoAppLifeCycleObserver(this._controller); bool

    _wasPlayingBeforePause = false; final VideoPlayerController _controller; void initialize() { WidgetsBinding.instance!.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.paused: _wasPlayingBeforePause = _controller.value.isPlaying; _controller.pause(); break; case AppLifecycleState.resumed: if (_wasPlayingBeforePause) { _controller.play(); } break; default: } } void dispose() { WidgetsBinding.instance!.removeObserver(this); } }
  18. ϥΠϑαΠΫϧʹ߹Θͤͯ࠶ੜʗఀࢭ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { _VideoAppLifeCycleObserver(this._controller); bool

    _wasPlayingBeforePause = false; final VideoPlayerController _controller; void initialize() { WidgetsBinding.instance!.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.paused: _wasPlayingBeforePause = _controller.value.isPlaying; _controller.pause(); break; case AppLifecycleState.resumed: if (_wasPlayingBeforePause) { _controller.play(); } break; default: } } void dispose() { WidgetsBinding.instance!.removeObserver(this); } }
  19. ग़དྷͳ͍͜ͱ • ը࣭ઃఆ • ϏοτϨʔτΛઃఆͰ͖ͳ͍ • ࣈນɾ෭Ի੠ • ετϦʔϜʹ৐ͬͯΔࣈນɾԻ੠ͷऔಘɺબ୒͕Ͱ͖ͳ͍ •

    UI • ࣗ෼Ͱ࣮૷͢Δඞཁ͕͋Δ • DRM flutter#24923 • Cookieͷઃఆ • ʢΤϥʔϋϯυϦϯάʣ
  20. ग़དྷͳ͍͜ͱ • ը࣭ઃఆ • ϏοτϨʔτΛઃఆͰ͖ͳ͍ • ࣈນɾ෭Ի੠ • ετϦʔϜʹ৐ͬͯΔࣈນɾԻ੠ͷऔಘɺબ୒͕Ͱ͖ͳ͍ •

    UI • ࣗ෼Ͱ࣮૷͢Δඞཁ͕͋Δ • DRM flutter#24923 • Cookieͷઃఆ • ʢΤϥʔϋϯυϦϯάʣ
  21. ग़དྷͳ͍͜ͱ • ը࣭ઃఆ • ϏοτϨʔτΛઃఆͰ͖ͳ͍ • ࣈນɾ෭Ի੠ • ετϦʔϜʹ৐ͬͯΔࣈນɾԻ੠ͷऔಘɺબ୒͕Ͱ͖ͳ͍ •

    UI • ࣗ෼Ͱ࣮૷͢Δඞཁ͕͋Δ • DRM flutter#24923 • Cookieͷઃఆ • ʢΤϥʔϋϯυϦϯάʣ
  22. ग़དྷͳ͍͜ͱ • ը࣭ઃఆ • ϏοτϨʔτΛઃఆͰ͖ͳ͍ • ࣈນɾ෭Ի੠ • ετϦʔϜʹ৐ͬͯΔࣈນɾԻ੠ͷऔಘɺબ୒͕Ͱ͖ͳ͍ •

    UI • ࣗ෼Ͱ࣮૷͢Δඞཁ͕͋Δ • DRM flutter#24923 • Cookieͷઃఆ • ʢΤϥʔϋϯυϦϯάʣ
  23. ग़དྷͳ͍͜ͱ • ը࣭ઃఆ • ϏοτϨʔτΛઃఆͰ͖ͳ͍ • ࣈນɾ෭Ի੠ • ετϦʔϜʹ৐ͬͯΔࣈນɾԻ੠ͷऔಘɺબ୒͕Ͱ͖ͳ͍ •

    UI • ࣗ෼Ͱ࣮૷͢Δඞཁ͕͋Δ • DRM flutter#24923 • Cookieͷઃఆ • ʢΤϥʔϋϯυϦϯάʣ
  24. ग़དྷͳ͍͜ͱ • ը࣭ઃఆ • ϏοτϨʔτΛઃఆͰ͖ͳ͍ • ࣈນɾ෭Ի੠ • ετϦʔϜʹ৐ͬͯΔࣈນɾԻ੠ͷऔಘɺબ୒͕Ͱ͖ͳ͍ •

    UI • ࣗ෼Ͱ࣮૷͢Δඞཁ͕͋Δ • DRM flutter#24923 • Cookieͷઃఆ • ʢΤϥʔϋϯυϦϯάʣ
  25. ग़དྷͳ͍͜ͱ • ը࣭ઃఆ • ϏοτϨʔτΛઃఆͰ͖ͳ͍ • ࣈນɾ෭Ի੠ • ετϦʔϜʹ৐ͬͯΔࣈນɾԻ੠ͷऔಘɺબ୒͕Ͱ͖ͳ͍ •

    UI • ࣗ෼Ͱ࣮૷͢Δඞཁ͕͋Δ • DRM flutter#24923 • Cookieͷઃఆ • ʢΤϥʔϋϯυϦϯάʣ
  26. ΤϥʔϋϯυϦϯά class VideoPlayerController extends ValueNotifier<VideoPlayerValue> { … } … class

    VideoPlayerValue { … final Duration duration; final Duration position; final Caption caption; final List<DurationRange> buffered; final bool isPlaying; final bool isLooping; final bool isBuffering; final double volume; final double playbackSpeed; final String? errorDescription; final Size size; final bool isInitialized; bool get hasError => errorDescription != null; … }
  27. ΤϥʔϋϯυϦϯά class VideoPlayerController extends ValueNotifier<VideoPlayerValue> { … } … class

    VideoPlayerValue { … final Duration duration; final Duration position; final Caption caption; final List<DurationRange> buffered; final bool isPlaying; final bool isLooping; final bool isBuffering; final double volume; final double playbackSpeed; final String? errorDescription; final Size size; final bool isInitialized; bool get hasError => errorDescription != null; … }
  28. ΤϥʔϋϯυϦϯά class VideoPlayerController extends ValueNotifier<VideoPlayerValue> { … } … class

    VideoPlayerValue { … final Duration duration; final Duration position; final Caption caption; final List<DurationRange> buffered; final bool isPlaying; final bool isLooping; final bool isBuffering; final double volume; final double playbackSpeed; final String? errorDescription; final Size size; final bool isInitialized; bool get hasError => errorDescription != null; … }
  29. ΤϥʔϋϯυϦϯά // iOS _eventSink([FlutterError errorWithCode:@"VideoError" message:@"Video cannot be fast-forwarded beyond

    2.0x" details:nil]); _eventSink([FlutterError errorWithCode:@"VideoError" message:[@"Failed to load video: “ stringByAppendingString:[item.error localizedDescription]] details:nil]); // Android eventSink.error("VideoError", "Video player had error " + error, null);
  30. ΤϥʔϋϯυϦϯά // iOS … AVPlayerItem* item = (AVPlayerItem*)object; switch (item.status)

    { case AVPlayerItemStatusFailed: if (_eventSink != nil) { _eventSink([FlutterError errorWithCode:@"VideoError" message:[@"Failed to load video: " stringByAppendingString:[item.error localizedDescription]] details:nil]); } break; case AVPlayerItemStatusUnknown: break; case AVPlayerItemStatusReadyToPlay: [item addOutput:_videoOutput]; [self sendInitialized]; [self updatePlayingState]; break; } …
  31. ΤϥʔϋϯυϦϯά // iOS … AVPlayerItem* item = (AVPlayerItem*)object; switch (item.status)

    { case AVPlayerItemStatusFailed: if (_eventSink != nil) { _eventSink([FlutterError errorWithCode:@"VideoError" message:[@"Failed to load video: " stringByAppendingString:[item.error localizedDescription]] details:nil]); } break; case AVPlayerItemStatusUnknown: break; case AVPlayerItemStatusReadyToPlay: [item addOutput:_videoOutput]; [self sendInitialized]; [self updatePlayingState]; break; } …
  32. ΤϥʔϋϯυϦϯά // Android exoPlayer.addListener( new EventListener() { … @Override public

    void onPlayerError(final ExoPlaybackException error) { setBuffering(false); if (eventSink != null) { eventSink.error("VideoError", "Video player had error " + error, null); } } });
  33. ΤϥʔϋϯυϦϯά // Android exoPlayer.addListener( new EventListener() { … @Override public

    void onPlayerError(final ExoPlaybackException error) { setBuffering(false); if (eventSink != null) { eventSink.error("VideoError", "Video player had error " + error, null); } } });
  34. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } }
  35. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } } class TextureMessage { int? textureId; Object encode() { final Map<Object?, Object?> pigeonMap = <Object?, Object?>{}; pigeonMap['textureId'] = textureId; return pigeonMap; } static TextureMessage decode(Object message) { final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>; return TextureMessage()..textureId = pigeonMap['textureId'] as int; } }
  36. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } } class TextureMessage { int? textureId; Object encode() { final Map<Object?, Object?> pigeonMap = <Object?, Object?>{}; pigeonMap['textureId'] = textureId; return pigeonMap; } static TextureMessage decode(Object message) { final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>; return TextureMessage()..textureId = pigeonMap['textureId'] as int; } }
  37. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } } class TextureMessage { int? textureId; Object encode() { final Map<Object?, Object?> pigeonMap = <Object?, Object?>{}; pigeonMap['textureId'] = textureId; return pigeonMap; } static TextureMessage decode(Object message) { final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>; return TextureMessage()..textureId = pigeonMap['textureId'] as int; } }
  38. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } } class TextureMessage { int? textureId; Object encode() { final Map<Object?, Object?> pigeonMap = <Object?, Object?>{}; pigeonMap['textureId'] = textureId; return pigeonMap; } static TextureMessage decode(Object message) { final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>; return TextureMessage()..textureId = pigeonMap['textureId'] as int; } }
  39. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } }
  40. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } } Channel͸APIຖʹ࡞੒
  41. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } }
  42. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } }
  43. BasicMessageChannel Future<PositionMessage> position(TextureMessage arg) async { final Object encoded =

    arg.encode(); const BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); final Map<Object?, Object?>? replyMap = await channel.send(encoded) as Map<Object?, Object?>?; if (replyMap == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel.', details: null, ); } else if (replyMap['error'] != null) { final Map<Object?, Object?> error = replyMap['error'] as Map<Object?, Object?>; throw PlatformException( code: error['code'] as String, message: error['message'] as String?, details: error['details'], ); } else { return PositionMessage.decode(replyMap['result']!); } }
  44. Dart // VideoPlayerController @override Widget build(BuildContext context) { return _textureId

    == VideoPlayerController.kUninitializedTextureId ? Container() : _videoPlayerPlatform.buildView(_textureId); } // video_player_platform_interface @override Widget buildView(int textureId) { return Texture(textureId: textureId); }
  45. Dart // VideoPlayerController @override Widget build(BuildContext context) { return _textureId

    == VideoPlayerController.kUninitializedTextureId ? Container() : _videoPlayerPlatform.buildView(_textureId); } // video_player_platform_interface @override Widget buildView(int textureId) { return Texture(textureId: textureId); }
  46. Dart // VideoPlayerController @override Widget build(BuildContext context) { return _textureId

    == VideoPlayerController.kUninitializedTextureId ? Container() : _videoPlayerPlatform.buildView(_textureId); } // video_player_platform_interface @override Widget buildView(int textureId) { return Texture(textureId: textureId); }
  47. iOS @protocol FlutterTexture <NSObject> /** Copy the contents of the

    texture into a `CVPixelBuffer`. */ - (CVPixelBufferRef _Nullable)copyPixelBuffer; /** * Called when the texture is unregistered. * * Called on the raster thread. */ @optional - (void)onTextureUnregistered:(NSObject<FlutterTexture>*)texture; @end FlutterTexture
  48. iOS @protocol FlutterTexture <NSObject> /** Copy the contents of the

    texture into a `CVPixelBuffer`. */ - (CVPixelBufferRef _Nullable)copyPixelBuffer; /** * Called when the texture is unregistered. * * Called on the raster thread. */ @optional - (void)onTextureUnregistered:(NSObject<FlutterTexture>*)texture; @end FlutterTexture
  49. iOS @protocol FlutterTexture <NSObject> /** Copy the contents of the

    texture into a `CVPixelBuffer`. */ - (CVPixelBufferRef _Nullable)copyPixelBuffer; /** * Called when the texture is unregistered. * * Called on the raster thread. */ @optional - (void)onTextureUnregistered:(NSObject<FlutterTexture>*)texture; @end FlutterTexture - (CVPixelBufferRef)copyPixelBuffer { CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { return [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; } else { return NULL; } }
  50. iOS @protocol FlutterTexture <NSObject> /** Copy the contents of the

    texture into a `CVPixelBuffer`. */ - (CVPixelBufferRef _Nullable)copyPixelBuffer; /** * Called when the texture is unregistered. * * Called on the raster thread. */ @optional - (void)onTextureUnregistered:(NSObject<FlutterTexture>*)texture; @end FlutterTexture - (CVPixelBufferRef)copyPixelBuffer { CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { return [_videoOutput copyPixelBufferForItemTime:outputItemTime itemTimeForDisplay:NULL]; } else { return NULL; } }
  51. iOS @protocol FlutterTexture <NSObject> /** Copy the contents of the

    texture into a `CVPixelBuffer`. */ - (CVPixelBufferRef _Nullable)copyPixelBuffer; /** * Called when the texture is unregistered. * * Called on the raster thread. */ @optional - (void)onTextureUnregistered:(NSObject<FlutterTexture>*)texture; @end FlutterTexture
  52. iOS @protocol FlutterTexture <NSObject> /** Copy the contents of the

    texture into a `CVPixelBuffer`. */ - (CVPixelBufferRef _Nullable)copyPixelBuffer; /** * Called when the texture is unregistered. * * Called on the raster thread. */ @optional - (void)onTextureUnregistered:(NSObject<FlutterTexture>*)texture; @end FlutterTexture - (void)onTextureUnregistered:(NSObject<FlutterTexture>*)texture { dispatch_async(dispatch_get_main_queue(), ^{ [self dispose]; }); }
  53. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry
  54. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry
  55. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry … int64_t textureId = [_registry registerTexture:player]; …
  56. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry … int64_t textureId = [_registry registerTexture:player]; … textureId͕ฦΔ
  57. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry
  58. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry • FlutterʹTexture͕ߋ৽͞Εͨͱ఻͑Δ • FlutterTexture.copyPixcelBuffer()͕ݺͼ͞ΕΔ
  59. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry _displayLink = [CADisplayLink displayLinkWithTarget:frameUpdater selector:@selector(onDisplayLink:)]; ""... - (void)onDisplayLink:(CADisplayLink*)link { [_registry textureFrameAvailable:_textureId]; }
  60. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry _displayLink = [CADisplayLink displayLinkWithTarget:frameUpdater selector:@selector(onDisplayLink:)]; ""... - (void)onDisplayLink:(CADisplayLink*)link { [_registry textureFrameAvailable:_textureId]; }
  61. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry _displayLink = [CADisplayLink displayLinkWithTarget:frameUpdater selector:@selector(onDisplayLink:)]; ""... - (void)onDisplayLink:(CADisplayLink*)link { [_registry textureFrameAvailable:_textureId]; }
  62. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry
  63. iOS @protocol FlutterTextureRegistry <NSObject> /** * Registers a `FlutterTexture` for

    usage in Flutter and returns an id that can be used to reference * that texture when calling into Flutter with channels. Textures must be registered on the * platform thread. */ - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture; /** * Notifies Flutter that the content of the previously registered texture has been updated. * * This will trigger a call to `-[FlutterTexture copyPixelBuffer]` on the raster thread. */ - (void)textureFrameAvailable:(int64_t)textureId; /** * Unregisters a `FlutterTexture` that has previously regeistered with `registerTexture:`. Textures * must be unregistered on the the platform thread. * * @param textureId The result that was previously returned from `registerTexture:`. */ - (void)unregisterTexture:(int64_t)textureId; @end FlutterTextureRegistry - (void)dispose:(FLTTextureMessage*)input error:(FlutterError**)error { … [_registry unregisterTexture:input.textureId.intValue];
  64. Android public interface TextureRegistry { /** * Creates and registers

    a SurfaceTexture managed by the Flutter engine. * * @return A SurfaceTextureEntry. */ SurfaceTextureEntry createSurfaceTexture(); /** A registry entry for a managed SurfaceTexture. */ interface SurfaceTextureEntry { /** @return The managed SurfaceTexture. */ SurfaceTexture surfaceTexture(); /** @return The identity of this SurfaceTexture. */ long id(); /** Deregisters and releases this SurfaceTexture. */ void release(); } } TextureRegistry
  65. Android public interface TextureRegistry { /** * Creates and registers

    a SurfaceTexture managed by the Flutter engine. * * @return A SurfaceTextureEntry. */ SurfaceTextureEntry createSurfaceTexture(); /** A registry entry for a managed SurfaceTexture. */ interface SurfaceTextureEntry { /** @return The managed SurfaceTexture. */ SurfaceTexture surfaceTexture(); /** @return The identity of this SurfaceTexture. */ long id(); /** Deregisters and releases this SurfaceTexture. */ void release(); } } TextureRegistry
  66. Android public interface TextureRegistry { /** * Creates and registers

    a SurfaceTexture managed by the Flutter engine. * * @return A SurfaceTextureEntry. */ SurfaceTextureEntry createSurfaceTexture(); /** A registry entry for a managed SurfaceTexture. */ interface SurfaceTextureEntry { /** @return The managed SurfaceTexture. */ SurfaceTexture surfaceTexture(); /** @return The identity of this SurfaceTexture. */ long id(); /** Deregisters and releases this SurfaceTexture. */ void release(); } } TextureRegistry ... TextureRegistry.SurfaceTextureEntry handle = flutterState.textureRegistry.createSurfaceTexture(); ...
  67. Android public interface TextureRegistry { /** * Creates and registers

    a SurfaceTexture managed by the Flutter engine. * * @return A SurfaceTextureEntry. */ SurfaceTextureEntry createSurfaceTexture(); /** A registry entry for a managed SurfaceTexture. */ interface SurfaceTextureEntry { /** @return The managed SurfaceTexture. */ SurfaceTexture surfaceTexture(); /** @return The identity of this SurfaceTexture. */ long id(); /** Deregisters and releases this SurfaceTexture. */ void release(); } } TextureRegistry
  68. Android public interface TextureRegistry { /** * Creates and registers

    a SurfaceTexture managed by the Flutter engine. * * @return A SurfaceTextureEntry. */ SurfaceTextureEntry createSurfaceTexture(); /** A registry entry for a managed SurfaceTexture. */ interface SurfaceTextureEntry { /** @return The managed SurfaceTexture. */ SurfaceTexture surfaceTexture(); /** @return The identity of this SurfaceTexture. */ long id(); /** Deregisters and releases this SurfaceTexture. */ void release(); } } TextureRegistry ... surface = new Surface(textureEntry.surfaceTexture()); exoPlayer.setVideoSurface(surface); ...
  69. Android public interface TextureRegistry { /** * Creates and registers

    a SurfaceTexture managed by the Flutter engine. * * @return A SurfaceTextureEntry. */ SurfaceTextureEntry createSurfaceTexture(); /** A registry entry for a managed SurfaceTexture. */ interface SurfaceTextureEntry { /** @return The managed SurfaceTexture. */ SurfaceTexture surfaceTexture(); /** @return The identity of this SurfaceTexture. */ long id(); /** Deregisters and releases this SurfaceTexture. */ void release(); } } TextureRegistry
  70. Android public interface TextureRegistry { /** * Creates and registers

    a SurfaceTexture managed by the Flutter engine. * * @return A SurfaceTextureEntry. */ SurfaceTextureEntry createSurfaceTexture(); /** A registry entry for a managed SurfaceTexture. */ interface SurfaceTextureEntry { /** @return The managed SurfaceTexture. */ SurfaceTexture surfaceTexture(); /** @return The identity of this SurfaceTexture. */ long id(); /** Deregisters and releases this SurfaceTexture. */ void release(); } } TextureRegistry textureId͕ฦΔ
  71. pigeon/messages.dart class LimitBitrateMessage { int textureId; int limitBitrate; } ...

    abstract class VideoPlayerApi { void initialize(); TextureMessage create(CreateMessage msg); void dispose(TextureMessage msg); void setLooping(LoopingMessage msg); void setVolume(VolumeMessage msg); void setPlaybackSpeed(PlaybackSpeedMessage msg); void play(TextureMessage msg); PositionMessage position(TextureMessage msg); void seekTo(PositionMessage msg); void pause(TextureMessage msg); void setMixWithOthers(MixWithOthersMessage msg); void setLimitBitrate(LimitBitrateMessage msg); }
  72. pigeon/messages.dart class LimitBitrateMessage { int textureId; int limitBitrate; } ...

    abstract class VideoPlayerApi { void initialize(); TextureMessage create(CreateMessage msg); void dispose(TextureMessage msg); void setLooping(LoopingMessage msg); void setVolume(VolumeMessage msg); void setPlaybackSpeed(PlaybackSpeedMessage msg); void play(TextureMessage msg); PositionMessage position(TextureMessage msg); void seekTo(PositionMessage msg); void pause(TextureMessage msg); void setMixWithOthers(MixWithOthersMessage msg); void setLimitBitrate(LimitBitrateMessage msg); }
  73. pigeon/messages.dart class LimitBitrateMessage { int textureId; int limitBitrate; } ...

    abstract class VideoPlayerApi { void initialize(); TextureMessage create(CreateMessage msg); void dispose(TextureMessage msg); void setLooping(LoopingMessage msg); void setVolume(VolumeMessage msg); void setPlaybackSpeed(PlaybackSpeedMessage msg); void play(TextureMessage msg); PositionMessage position(TextureMessage msg); void seekTo(PositionMessage msg); void pause(TextureMessage msg); void setMixWithOthers(MixWithOthersMessage msg); void setLimitBitrate(LimitBitrateMessage msg); }
  74. ϑΝΠϧͷੜ੒ $flutter pub run pigeon --input pigeons/messages.dart —dart_null_safety $(cd ../../../;

    ./script/incremental_build.sh format --travis --clang-format=clang-format-7)
  75. ϑΝΠϧͷੜ੒ $flutter pub run pigeon --input pigeons/messages.dart —dart_null_safety $(cd ../../../;

    ./script/incremental_build.sh format --travis --clang-format=clang-format-7)
  76. ϑΝΠϧͷੜ੒ $flutter pub run pigeon --input pigeons/messages.dart —dart_null_safety $(cd ../../../;

    ./script/incremental_build.sh format --travis --clang-format=clang-format-7)
  77. ϑΝΠϧͷੜ੒ $flutter pub run pigeon --input pigeons/messages.dart —dart_null_safety $(cd ../../../;

    ./script/incremental_build.sh format --travis --clang-format=clang-format-7) flutter/plugin ಠࣗͷϑΥʔϚοτ