From 122587d26b93176e9d4aabfdd9158f159a848ee4 Mon Sep 17 00:00:00 2001 From: hyue7 <111051566+hyue7@users.noreply.github.com> Date: Fri, 10 Nov 2023 14:02:56 +0800 Subject: [PATCH] [video_player_videohole] Implement video, audio and text track selections. (#607) Co-authored-by: Swift Kim --- packages/video_player_videohole/CHANGELOG.md | 4 + packages/video_player_videohole/README.md | 2 +- .../example/lib/main.dart | 198 ++++++++++++++++- .../lib/src/messages.g.dart | 149 ++++++++++++- .../lib/src/tracks.dart | 127 +++++++++++ .../lib/src/video_player_tizen.dart | 112 ++++++++++ .../lib/video_player.dart | 41 ++++ .../lib/video_player_platform_interface.dart | 21 ++ .../pigeons/messages.dart | 21 ++ packages/video_player_videohole/pubspec.yaml | 2 +- .../tizen/src/messages.cc | 204 ++++++++++++++++- .../tizen/src/messages.h | 73 ++++++ .../tizen/src/video_player.cc | 208 ++++++++++++++++++ .../tizen/src/video_player.h | 40 ++++ .../tizen/src/video_player_tizen_plugin.cc | 26 +++ 15 files changed, 1223 insertions(+), 5 deletions(-) create mode 100644 packages/video_player_videohole/lib/src/tracks.dart diff --git a/packages/video_player_videohole/CHANGELOG.md b/packages/video_player_videohole/CHANGELOG.md index 2c6d16f51..07752697b 100644 --- a/packages/video_player_videohole/CHANGELOG.md +++ b/packages/video_player_videohole/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.2.0 + +* Implement functionality of selecting video, audio and text tracks. + ## 0.1.3 * Fix event channel issue, sending messages from native to Flutter on the platform thread. diff --git a/packages/video_player_videohole/README.md b/packages/video_player_videohole/README.md index c2bb54393..252c1cd97 100644 --- a/packages/video_player_videohole/README.md +++ b/packages/video_player_videohole/README.md @@ -12,7 +12,7 @@ To use this package, add `video_player_videohole` as a dependency in your `pubsp ```yaml dependencies: - video_player_videohole: ^0.1.3 + video_player_videohole: ^0.2.0 ``` Then you can import `video_player_videohole` in your Dart code: diff --git a/packages/video_player_videohole/example/lib/main.dart b/packages/video_player_videohole/example/lib/main.dart index 24f6a7c90..873360c4d 100644 --- a/packages/video_player_videohole/example/lib/main.dart +++ b/packages/video_player_videohole/example/lib/main.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: public_member_api_docs, avoid_print +// ignore_for_file: public_member_api_docs, avoid_print, use_build_context_synchronously /// An example of using the plugin, controlling lifecycle and playback of the /// video. @@ -37,6 +37,7 @@ class _App extends StatelessWidget { Tab(icon: Icon(Icons.cloud), text: 'Dash'), Tab(icon: Icon(Icons.cloud), text: 'DRM Widevine'), Tab(icon: Icon(Icons.cloud), text: 'DRM PlayReady'), + Tab(icon: Icon(Icons.cloud), text: 'Track'), ], ), ), @@ -47,6 +48,7 @@ class _App extends StatelessWidget { _DashRomoteVideo(), _DrmRemoteVideo(), _DrmRemoteVideo2(), + _TrackTest(), ], ), ), @@ -370,6 +372,69 @@ class _DrmRemoteVideoState2 extends State<_DrmRemoteVideo2> { } } +class _TrackTest extends StatefulWidget { + @override + State<_TrackTest> createState() => _TrackTestState(); +} + +class _TrackTestState extends State<_TrackTest> { + late VideoPlayerController _controller; + + @override + void initState() { + super.initState(); + + _controller = VideoPlayerController.network( + 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8'); + + _controller.addListener(() { + if (_controller.value.hasError) { + print(_controller.value.errorDescription); + } + setState(() {}); + }); + _controller.setLooping(true); + _controller.initialize().then((_) => setState(() {})); + _controller.play(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('track selections test'), + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller), + ClosedCaption(text: _controller.value.caption.text), + _ControlsOverlay(controller: _controller), + VideoProgressIndicator(_controller, allowScrubbing: true), + ], + ), + ), + ), + _GetVideoTrackButton(controller: _controller), + _GetAudioTrackButton(controller: _controller), + _GetTextTrackButton(controller: _controller), + ], + ), + ); + } +} + class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({required this.controller}); @@ -485,3 +550,134 @@ class _ControlsOverlay extends StatelessWidget { ); } } + +class _GetVideoTrackButton extends StatelessWidget { + const _GetVideoTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Video Track'), + onPressed: () async { + final List? videotracks = await controller.videoTracks; + if (videotracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Video'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: videotracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + '${videotracks[index].width}x${videotracks[index].height},${(videotracks[index].bitrate / 1000000).toStringAsFixed(2)}Mbps'), + onTap: () { + controller + .setTrackSelection(videotracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} + +class _GetAudioTrackButton extends StatelessWidget { + const _GetAudioTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Audio Track'), + onPressed: () async { + final List? audioTracks = await controller.audioTracks; + if (audioTracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Audio'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: audioTracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + 'language:${audioTracks[index].language}'), + onTap: () { + controller + .setTrackSelection(audioTracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} + +class _GetTextTrackButton extends StatelessWidget { + const _GetTextTrackButton({required this.controller}); + + final VideoPlayerController controller; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(top: 20.0), + child: MaterialButton( + child: const Text('Get Text Track'), + onPressed: () async { + final List? textTracks = await controller.textTracks; + if (textTracks == null) { + return; + } + await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Text'), + content: SizedBox( + height: 200, + width: 200, + child: ListView.builder( + itemCount: textTracks.length, + itemBuilder: (BuildContext context, int index) { + return ListTile( + title: Text( + 'language:${textTracks[index].language}'), + onTap: () { + controller.setTrackSelection(textTracks[index]); + }, + ); + }, + )), + ); + }); + }), + ); + } +} diff --git a/packages/video_player_videohole/lib/src/messages.g.dart b/packages/video_player_videohole/lib/src/messages.g.dart index 1dcbd4698..ea6b377ce 100644 --- a/packages/video_player_videohole/lib/src/messages.g.dart +++ b/packages/video_player_videohole/lib/src/messages.g.dart @@ -107,6 +107,89 @@ class PlaybackSpeedMessage { } } +class TrackMessage { + TrackMessage({ + required this.playerId, + required this.tracks, + }); + + int playerId; + + List?> tracks; + + Object encode() { + return [ + playerId, + tracks, + ]; + } + + static TrackMessage decode(Object result) { + result as List; + return TrackMessage( + playerId: result[0]! as int, + tracks: (result[1] as List?)!.cast?>(), + ); + } +} + +class TrackTypeMessage { + TrackTypeMessage({ + required this.playerId, + required this.trackType, + }); + + int playerId; + + int trackType; + + Object encode() { + return [ + playerId, + trackType, + ]; + } + + static TrackTypeMessage decode(Object result) { + result as List; + return TrackTypeMessage( + playerId: result[0]! as int, + trackType: result[1]! as int, + ); + } +} + +class SelectedTracksMessage { + SelectedTracksMessage({ + required this.playerId, + required this.trackId, + required this.trackType, + }); + + int playerId; + + int trackId; + + int trackType; + + Object encode() { + return [ + playerId, + trackId, + trackType, + ]; + } + + static SelectedTracksMessage decode(Object result) { + result as List; + return SelectedTracksMessage( + playerId: result[0]! as int, + trackId: result[1]! as int, + trackType: result[2]! as int, + ); + } +} + class PositionMessage { PositionMessage({ required this.playerId, @@ -268,9 +351,18 @@ class _VideoPlayerVideoholeApiCodec extends StandardMessageCodec { } else if (value is PositionMessage) { buffer.putUint8(134); writeValue(buffer, value.encode()); - } else if (value is VolumeMessage) { + } else if (value is SelectedTracksMessage) { buffer.putUint8(135); writeValue(buffer, value.encode()); + } else if (value is TrackMessage) { + buffer.putUint8(136); + writeValue(buffer, value.encode()); + } else if (value is TrackTypeMessage) { + buffer.putUint8(137); + writeValue(buffer, value.encode()); + } else if (value is VolumeMessage) { + buffer.putUint8(138); + writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); } @@ -294,6 +386,12 @@ class _VideoPlayerVideoholeApiCodec extends StandardMessageCodec { case 134: return PositionMessage.decode(readValue(buffer)!); case 135: + return SelectedTracksMessage.decode(readValue(buffer)!); + case 136: + return TrackMessage.decode(readValue(buffer)!); + case 137: + return TrackTypeMessage.decode(readValue(buffer)!); + case 138: return VolumeMessage.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -518,6 +616,55 @@ class VideoPlayerVideoholeApi { } } + Future track(TrackTypeMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerVideoholeApi.track', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as TrackMessage?)!; + } + } + + Future setTrackSelection(SelectedTracksMessage arg_msg) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerVideoholeApi.setTrackSelection', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_msg]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + Future pause(PlayerMessage arg_msg) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerVideoholeApi.pause', codec, diff --git a/packages/video_player_videohole/lib/src/tracks.dart b/packages/video_player_videohole/lib/src/tracks.dart new file mode 100644 index 000000000..fe68ab1df --- /dev/null +++ b/packages/video_player_videohole/lib/src/tracks.dart @@ -0,0 +1,127 @@ +// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Type of the track. +enum TrackType { + /// The video track. + video, + + /// The audio track. + audio, + + /// The text track. + text, +} + +/// Type of the track audio channel for [TrackType.audio]. +enum AudioTrackChannelType { + /// The mono channel. + mono, + + /// The stereo channel. + stereo, + + /// The surround channel. + surround, +} + +/// Type of the track subtitle type for [TrackType.text]. +enum TextTrackSubtitleType { + /// The text subtitle. + text, + + /// The picture subtitle. + picture, +} + +/// A representation of a single track. +/// +/// A typical video file will include several [Track]s.Such as [VideoTrack]s, [AudioTrack]s, [TextTrack]s. +class Track { + /// Creates an instance of [Track]. + /// + /// The [trackId] and [trackType] arguments are required. + /// + const Track({ + required this.trackId, + required this.trackType, + }); + + /// The track id of track that uses to determine track. + final int trackId; + + /// The type of the track. + final TrackType trackType; +} + +/// A representation of a video track. +class VideoTrack extends Track { + /// Creates an instance of [VideoTrack]. + /// + /// The [width], [height] and [bitrate] argument is required. + /// + /// [trackType] is [TrackType.video]. + VideoTrack({ + required super.trackId, + super.trackType = TrackType.video, + required this.width, + required this.height, + required this.bitrate, + }); + + /// The width of video track. + final int width; + + /// The height of video track. + final int height; + + /// The bitrate of video track. + final int bitrate; +} + +/// A representation of a audio track. +class AudioTrack extends Track { + /// Creates an instance of [AudioTrack]. + /// + /// The [language], [channel] and [bitrate] arguments are required. + /// + /// [trackType] is [TrackType.audio]. + AudioTrack({ + required super.trackId, + super.trackType = TrackType.audio, + required this.language, + required this.channel, + required this.bitrate, + }); + + /// The language of audio track. + final String language; + + /// The channel of audio track. + final AudioTrackChannelType channel; + + /// The bitrate of audio track. + final int bitrate; +} + +/// A representation of a text track. +class TextTrack extends Track { + /// Creates an instance of [TextTrack]. + /// + /// The [language] and [subtitleType] arguments are required. + /// + /// [trackType] is [TrackType.text]. + TextTrack({ + required super.trackId, + super.trackType = TrackType.text, + required this.language, + required this.subtitleType, + }); + + /// The language of text track. + final String language; + + /// The subtitle type of track. + final TextTrackSubtitleType subtitleType; +} diff --git a/packages/video_player_videohole/lib/src/video_player_tizen.dart b/packages/video_player_videohole/lib/src/video_player_tizen.dart index 2e5427472..94dc871af 100644 --- a/packages/video_player_videohole/lib/src/video_player_tizen.dart +++ b/packages/video_player_videohole/lib/src/video_player_tizen.dart @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart'; import '../video_player_platform_interface.dart'; import 'messages.g.dart'; +import 'tracks.dart'; /// An implementation of [VideoPlayerPlatform] that uses the /// Pigeon-generated [TizenVideoPlayerApi]. @@ -88,6 +89,98 @@ class VideoPlayerTizen extends VideoPlayerPlatform { PositionMessage(playerId: playerId, position: position.inMilliseconds)); } + @override + Future> getVideoTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == TrackType.video, + orElse: () => -1), + )); + + final List videoTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final int bitrate = trackMap['bitrate']! as int; + final int width = trackMap['width']! as int; + final int height = trackMap['height']! as int; + + videoTracks.add(VideoTrack( + trackId: trackId, + width: width, + height: height, + bitrate: bitrate, + )); + } + + return videoTracks; + } + + @override + Future> getAudioTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == TrackType.audio, + orElse: () => -1), + )); + + final List audioTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final String language = trackMap['language']! as String; + final AudioTrackChannelType channelType = + _intChannelTypeMap[trackMap['channel']]!; + final int bitrate = trackMap['bitrate']! as int; + + audioTracks.add(AudioTrack( + trackId: trackId, + language: language, + channel: channelType, + bitrate: bitrate, + )); + } + + return audioTracks; + } + + @override + Future> getTextTracks(int playerId) async { + final TrackMessage response = await _api.track(TrackTypeMessage( + playerId: playerId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == TrackType.text, + orElse: () => -1), + )); + + final List textTracks = []; + for (final Map? trackMap in response.tracks) { + final int trackId = trackMap!['trackId']! as int; + final String language = trackMap['language']! as String; + final TextTrackSubtitleType subtitleType = + _intSubtitleTypeMap[trackMap['subtitleType']]!; + + textTracks.add(TextTrack( + trackId: trackId, + language: language, + subtitleType: subtitleType, + )); + } + + return textTracks; + } + + @override + Future setTrackSelection(int playerId, Track track) { + return _api.setTrackSelection(SelectedTracksMessage( + playerId: playerId, + trackId: track.trackId, + trackType: _intTrackTypeMap.keys.firstWhere( + (int key) => _intTrackTypeMap[key] == track.trackType, + orElse: () => -1), + )); + } + @override Future getPosition(int playerId) async { final PositionMessage response = @@ -174,4 +267,23 @@ class VideoPlayerTizen extends VideoPlayerPlatform { VideoFormat.dash: 'dash', VideoFormat.other: 'other', }; + + static const Map _intTrackTypeMap = { + 1: TrackType.audio, + 2: TrackType.video, + 3: TrackType.text, + }; + + static const Map _intChannelTypeMap = + { + 1: AudioTrackChannelType.mono, + 2: AudioTrackChannelType.stereo, + 3: AudioTrackChannelType.surround, + }; + + static const Map _intSubtitleTypeMap = + { + 0: TextTrackSubtitleType.text, + 1: TextTrackSubtitleType.picture, + }; } diff --git a/packages/video_player_videohole/lib/video_player.dart b/packages/video_player_videohole/lib/video_player.dart index d510253f3..5f8d7beaf 100644 --- a/packages/video_player_videohole/lib/video_player.dart +++ b/packages/video_player_videohole/lib/video_player.dart @@ -15,10 +15,12 @@ import 'src/drm_configs.dart'; import 'src/hole.dart'; import 'src/register_drm_callback_stub.dart' if (dart.library.ffi) 'src/register_drm_callback_real.dart'; +import 'src/tracks.dart'; import 'video_player_platform_interface.dart'; export 'src/closed_caption_file.dart'; export 'src/drm_configs.dart'; +export 'src/tracks.dart'; VideoPlayerPlatform? _lastVideoPlayerPlatform; @@ -44,6 +46,7 @@ class VideoPlayerValue { this.position = Duration.zero, this.caption = Caption.none, this.captionOffset = Duration.zero, + this.tracks = const [], this.buffered = 0, this.isInitialized = false, this.isPlaying = false, @@ -106,6 +109,9 @@ class VideoPlayerValue { /// The current speed of the playback. final double playbackSpeed; + /// The current playback tracks. + final List tracks; + /// A description of the error if present. /// /// If [hasError] is false this is `null`. @@ -146,6 +152,7 @@ class VideoPlayerValue { Duration? position, Caption? caption, Duration? captionOffset, + List? tracks, int? buffered, bool? isInitialized, bool? isPlaying, @@ -161,6 +168,7 @@ class VideoPlayerValue { position: position ?? this.position, caption: caption ?? this.caption, captionOffset: captionOffset ?? this.captionOffset, + tracks: tracks ?? this.tracks, buffered: buffered ?? this.buffered, isInitialized: isInitialized ?? this.isInitialized, isPlaying: isPlaying ?? this.isPlaying, @@ -182,6 +190,7 @@ class VideoPlayerValue { 'position: $position, ' 'caption: $caption, ' 'captionOffset: $captionOffset, ' + 'tracks: $tracks, ' 'buffered: $buffered, ' 'isInitialized: $isInitialized, ' 'isPlaying: $isPlaying, ' @@ -577,6 +586,38 @@ class VideoPlayerController extends ValueNotifier { _updatePosition(position); } + /// The video tracks in the current video. + Future?> get videoTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getVideoTracks(_playerId); + } + + /// The audio tracks in the current video. + Future?> get audioTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getAudioTracks(_playerId); + } + + /// The text tracks in the current video. + Future?> get textTracks async { + if (!value.isInitialized || _isDisposed) { + return null; + } + return _videoPlayerPlatform.getTextTracks(_playerId); + } + + /// Sets the selected tracks. + Future setTrackSelection(Track track) async { + if (!value.isInitialized || _isDisposed) { + return; + } + await _videoPlayerPlatform.setTrackSelection(_playerId, track); + } + /// Sets the audio volume of [this]. /// /// [volume] indicates a value between 0.0 (silent) and 1.0 (full volume) on a diff --git a/packages/video_player_videohole/lib/video_player_platform_interface.dart b/packages/video_player_videohole/lib/video_player_platform_interface.dart index 6d87887fa..e8e63ce21 100644 --- a/packages/video_player_videohole/lib/video_player_platform_interface.dart +++ b/packages/video_player_videohole/lib/video_player_platform_interface.dart @@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'src/drm_configs.dart'; +import 'src/tracks.dart'; import 'src/video_player_tizen.dart'; /// The interface that implementations of video_player must implement. @@ -86,6 +87,26 @@ abstract class VideoPlayerPlatform extends PlatformInterface { throw UnimplementedError('seekTo() has not been implemented.'); } + /// Gets the video tracks as a list of [VideoTrack]. + Future> getVideoTracks(int playerId) { + throw UnimplementedError('getVideoTracks() has not been implemented.'); + } + + /// Gets the audio tracks as a list of [AudioTrack]. + Future> getAudioTracks(int playerId) { + throw UnimplementedError('getAudioTracks() has not been implemented.'); + } + + /// Gets the text tracks as a list of [TextTrack]. + Future> getTextTracks(int playerId) { + throw UnimplementedError('getTextTracks() has not been implemented.'); + } + + /// Sets the selected track. + Future setTrackSelection(int playerId, Track track) { + throw UnimplementedError('setTrackSelection() has not been implemented.'); + } + /// Sets the playback speed to a [speed] value indicating the playback rate. Future setPlaybackSpeed(int playerId, double speed) { throw UnimplementedError('setPlaybackSpeed() has not been implemented.'); diff --git a/packages/video_player_videohole/pigeons/messages.dart b/packages/video_player_videohole/pigeons/messages.dart index 1a7e946f9..25bf8173b 100644 --- a/packages/video_player_videohole/pigeons/messages.dart +++ b/packages/video_player_videohole/pigeons/messages.dart @@ -32,6 +32,25 @@ class PlaybackSpeedMessage { double speed; } +class TrackMessage { + TrackMessage(this.playerId, this.tracks); + int playerId; + List?> tracks; +} + +class TrackTypeMessage { + TrackTypeMessage(this.playerId, this.trackType); + int playerId; + int trackType; +} + +class SelectedTracksMessage { + SelectedTracksMessage(this.playerId, this.trackId, this.trackType); + int playerId; + int trackId; + int trackType; +} + class PositionMessage { PositionMessage(this.playerId, this.position); int playerId; @@ -74,6 +93,8 @@ abstract class VideoPlayerVideoholeApi { PositionMessage position(PlayerMessage msg); @async void seekTo(PositionMessage msg); + TrackMessage track(TrackTypeMessage msg); + void setTrackSelection(SelectedTracksMessage msg); void pause(PlayerMessage msg); void setMixWithOthers(MixWithOthersMessage msg); void setDisplayGeometry(GeometryMessage msg); diff --git a/packages/video_player_videohole/pubspec.yaml b/packages/video_player_videohole/pubspec.yaml index 881e4da35..876ebac9d 100644 --- a/packages/video_player_videohole/pubspec.yaml +++ b/packages/video_player_videohole/pubspec.yaml @@ -2,7 +2,7 @@ name: video_player_videohole description: Flutter plugin for displaying inline video on Tizen TV devices. homepage: https://github.com/flutter-tizen/plugins repository: https://github.com/flutter-tizen/plugins/tree/master/packages/video_player_videohole -version: 0.1.3 +version: 0.2.0 environment: sdk: ">=2.18.0 <4.0.0" diff --git a/packages/video_player_videohole/tizen/src/messages.cc b/packages/video_player_videohole/tizen/src/messages.cc index 80ddfce8e..090d888ff 100644 --- a/packages/video_player_videohole/tizen/src/messages.cc +++ b/packages/video_player_videohole/tizen/src/messages.cc @@ -123,6 +123,106 @@ PlaybackSpeedMessage PlaybackSpeedMessage::FromEncodableList( return decoded; } +// TrackMessage + +TrackMessage::TrackMessage(int64_t player_id, const EncodableList& tracks) + : player_id_(player_id), tracks_(tracks) {} + +int64_t TrackMessage::player_id() const { return player_id_; } + +void TrackMessage::set_player_id(int64_t value_arg) { player_id_ = value_arg; } + +const EncodableList& TrackMessage::tracks() const { return tracks_; } + +void TrackMessage::set_tracks(const EncodableList& value_arg) { + tracks_ = value_arg; +} + +EncodableList TrackMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(tracks_)); + return list; +} + +TrackMessage TrackMessage::FromEncodableList(const EncodableList& list) { + TrackMessage decoded(list[0].LongValue(), std::get(list[1])); + return decoded; +} + +// TrackTypeMessage + +TrackTypeMessage::TrackTypeMessage(int64_t player_id, int64_t track_type) + : player_id_(player_id), track_type_(track_type) {} + +int64_t TrackTypeMessage::player_id() const { return player_id_; } + +void TrackTypeMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +int64_t TrackTypeMessage::track_type() const { return track_type_; } + +void TrackTypeMessage::set_track_type(int64_t value_arg) { + track_type_ = value_arg; +} + +EncodableList TrackTypeMessage::ToEncodableList() const { + EncodableList list; + list.reserve(2); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(track_type_)); + return list; +} + +TrackTypeMessage TrackTypeMessage::FromEncodableList( + const EncodableList& list) { + TrackTypeMessage decoded(list[0].LongValue(), list[1].LongValue()); + return decoded; +} + +// SelectedTracksMessage + +SelectedTracksMessage::SelectedTracksMessage(int64_t player_id, + int64_t track_id, + int64_t track_type) + : player_id_(player_id), track_id_(track_id), track_type_(track_type) {} + +int64_t SelectedTracksMessage::player_id() const { return player_id_; } + +void SelectedTracksMessage::set_player_id(int64_t value_arg) { + player_id_ = value_arg; +} + +int64_t SelectedTracksMessage::track_id() const { return track_id_; } + +void SelectedTracksMessage::set_track_id(int64_t value_arg) { + track_id_ = value_arg; +} + +int64_t SelectedTracksMessage::track_type() const { return track_type_; } + +void SelectedTracksMessage::set_track_type(int64_t value_arg) { + track_type_ = value_arg; +} + +EncodableList SelectedTracksMessage::ToEncodableList() const { + EncodableList list; + list.reserve(3); + list.push_back(EncodableValue(player_id_)); + list.push_back(EncodableValue(track_id_)); + list.push_back(EncodableValue(track_type_)); + return list; +} + +SelectedTracksMessage SelectedTracksMessage::FromEncodableList( + const EncodableList& list) { + SelectedTracksMessage decoded(list[0].LongValue(), list[1].LongValue(), + list[2].LongValue()); + return decoded; +} + // PositionMessage PositionMessage::PositionMessage(int64_t player_id, int64_t position) @@ -388,6 +488,15 @@ EncodableValue VideoPlayerVideoholeApiCodecSerializer::ReadValueOfType( return CustomEncodableValue(PositionMessage::FromEncodableList( std::get(ReadValue(stream)))); case 135: + return CustomEncodableValue(SelectedTracksMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 136: + return CustomEncodableValue(TrackMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 137: + return CustomEncodableValue(TrackTypeMessage::FromEncodableList( + std::get(ReadValue(stream)))); + case 138: return CustomEncodableValue(VolumeMessage::FromEncodableList( std::get(ReadValue(stream)))); default: @@ -455,8 +564,32 @@ void VideoPlayerVideoholeApiCodecSerializer::WriteValue( stream); return; } - if (custom_value->type() == typeid(VolumeMessage)) { + if (custom_value->type() == typeid(SelectedTracksMessage)) { stream->WriteByte(135); + WriteValue( + EncodableValue(std::any_cast(*custom_value) + .ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TrackMessage)) { + stream->WriteByte(136); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(TrackTypeMessage)) { + stream->WriteByte(137); + WriteValue( + EncodableValue( + std::any_cast(*custom_value).ToEncodableList()), + stream); + return; + } + if (custom_value->type() == typeid(VolumeMessage)) { + stream->WriteByte(138); WriteValue( EncodableValue( std::any_cast(*custom_value).ToEncodableList()), @@ -772,6 +905,75 @@ void VideoPlayerVideoholeApi::SetUp(flutter::BinaryMessenger* binary_messenger, channel->SetMessageHandler(nullptr); } } + { + auto channel = std::make_unique>( + binary_messenger, "dev.flutter.pigeon.VideoPlayerVideoholeApi.track", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + ErrorOr output = api->Track(msg_arg); + if (output.has_error()) { + reply(WrapError(output.error())); + return; + } + EncodableList wrapped; + wrapped.push_back( + CustomEncodableValue(std::move(output).TakeValue())); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } + { + auto channel = std::make_unique>( + binary_messenger, + "dev.flutter.pigeon.VideoPlayerVideoholeApi.setTrackSelection", + &GetCodec()); + if (api != nullptr) { + channel->SetMessageHandler( + [api](const EncodableValue& message, + const flutter::MessageReply& reply) { + try { + const auto& args = std::get(message); + const auto& encodable_msg_arg = args.at(0); + if (encodable_msg_arg.IsNull()) { + reply(WrapError("msg_arg unexpectedly null.")); + return; + } + const auto& msg_arg = std::any_cast( + std::get(encodable_msg_arg)); + std::optional output = + api->SetTrackSelection(msg_arg); + if (output.has_value()) { + reply(WrapError(output.value())); + return; + } + EncodableList wrapped; + wrapped.push_back(EncodableValue()); + reply(EncodableValue(std::move(wrapped))); + } catch (const std::exception& exception) { + reply(WrapError(exception.what())); + } + }); + } else { + channel->SetMessageHandler(nullptr); + } + } { auto channel = std::make_unique>( binary_messenger, "dev.flutter.pigeon.VideoPlayerVideoholeApi.pause", diff --git a/packages/video_player_videohole/tizen/src/messages.h b/packages/video_player_videohole/tizen/src/messages.h index 6effb80c4..3a40ddbb3 100644 --- a/packages/video_player_videohole/tizen/src/messages.h +++ b/packages/video_player_videohole/tizen/src/messages.h @@ -134,6 +134,76 @@ class PlaybackSpeedMessage { double speed_; }; +// Generated class from Pigeon that represents data sent in messages. +class TrackMessage { + public: + // Constructs an object setting all fields. + explicit TrackMessage(int64_t player_id, + const flutter::EncodableList& tracks); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + const flutter::EncodableList& tracks() const; + void set_tracks(const flutter::EncodableList& value_arg); + + private: + static TrackMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerVideoholeApi; + friend class VideoPlayerVideoholeApiCodecSerializer; + int64_t player_id_; + flutter::EncodableList tracks_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class TrackTypeMessage { + public: + // Constructs an object setting all fields. + explicit TrackTypeMessage(int64_t player_id, int64_t track_type); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + int64_t track_type() const; + void set_track_type(int64_t value_arg); + + private: + static TrackTypeMessage FromEncodableList(const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerVideoholeApi; + friend class VideoPlayerVideoholeApiCodecSerializer; + int64_t player_id_; + int64_t track_type_; +}; + +// Generated class from Pigeon that represents data sent in messages. +class SelectedTracksMessage { + public: + // Constructs an object setting all fields. + explicit SelectedTracksMessage(int64_t player_id, int64_t track_id, + int64_t track_type); + + int64_t player_id() const; + void set_player_id(int64_t value_arg); + + int64_t track_id() const; + void set_track_id(int64_t value_arg); + + int64_t track_type() const; + void set_track_type(int64_t value_arg); + + private: + static SelectedTracksMessage FromEncodableList( + const flutter::EncodableList& list); + flutter::EncodableList ToEncodableList() const; + friend class VideoPlayerVideoholeApi; + friend class VideoPlayerVideoholeApiCodecSerializer; + int64_t player_id_; + int64_t track_id_; + int64_t track_type_; +}; + // Generated class from Pigeon that represents data sent in messages. class PositionMessage { public: @@ -293,6 +363,9 @@ class VideoPlayerVideoholeApi { virtual void SeekTo( const PositionMessage& msg, std::function reply)> result) = 0; + virtual ErrorOr Track(const TrackTypeMessage& msg) = 0; + virtual std::optional SetTrackSelection( + const SelectedTracksMessage& msg) = 0; virtual std::optional Pause(const PlayerMessage& msg) = 0; virtual std::optional SetMixWithOthers( const MixWithOthersMessage& msg) = 0; diff --git a/packages/video_player_videohole/tizen/src/video_player.cc b/packages/video_player_videohole/tizen/src/video_player.cc index 85786ed37..81d1df2e7 100644 --- a/packages/video_player_videohole/tizen/src/video_player.cc +++ b/packages/video_player_videohole/tizen/src/video_player.cc @@ -311,6 +311,214 @@ void VideoPlayer::SetPlaybackSpeed(double speed) { } } +flutter::EncodableList VideoPlayer::getTrackInfo(int32_t track_type) { + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_state failed: %s", + get_error_message(ret)); + return {}; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[VideoPlayer] Player not ready."); + return {}; + } + + void *player_lib_handle = dlopen("libcapi-media-player.so.0", RTLD_LAZY); + if (!player_lib_handle) { + LOG_ERROR("[VideoPlayer] dlopen failed: %s", dlerror()); + return {}; + } + + FuncPlayerGetTrackCountV2 player_get_track_count_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_track_count_v2")); + if (!player_get_track_count_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + int track_count = 0; + ret = player_get_track_count_v2(player_, (player_stream_type_e)track_type, + &track_count); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_track_count_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + if (track_count <= 0) { + return {}; + } + + flutter::EncodableList trackSelections = {}; + if (track_type == PLAYER_STREAM_TYPE_VIDEO) { + LOG_INFO("[VideoPlayer] video_count: %d", track_count); + + FuncPlayerGetVideoTrackInfoV2 player_get_video_track_info_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_video_track_info_v2")); + if (!player_get_video_track_info_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + for (int video_index = 0; video_index < track_count; video_index++) { + flutter::EncodableMap trackSelection = {}; + player_video_track_info_v2 *video_track_info = nullptr; + + ret = player_get_video_track_info_v2(player_, video_index, + &video_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_video_track_info_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + LOG_INFO( + "[VideoPlayer] video track info: width[%d], height[%d], " + "bitrate[%d]", + video_track_info->width, video_track_info->height, + video_track_info->bit_rate); + + trackSelection.insert( + {flutter::EncodableValue("trackType"), + flutter::EncodableValue(PLAYER_STREAM_TYPE_VIDEO)}); + trackSelection.insert({flutter::EncodableValue("trackId"), + flutter::EncodableValue(video_index)}); + trackSelection.insert({flutter::EncodableValue("width"), + flutter::EncodableValue(video_track_info->width)}); + trackSelection.insert( + {flutter::EncodableValue("height"), + flutter::EncodableValue(video_track_info->height)}); + trackSelection.insert( + {flutter::EncodableValue("bitrate"), + flutter::EncodableValue(video_track_info->bit_rate)}); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + + } else if (track_type == PLAYER_STREAM_TYPE_AUDIO) { + LOG_INFO("[VideoPlayer] audio_count: %d", track_count); + + FuncPlayerGetAudioTrackInfoV2 player_get_audio_track_info_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_audio_track_info_v2")); + if (!player_get_audio_track_info_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + for (int audio_index = 0; audio_index < track_count; audio_index++) { + flutter::EncodableMap trackSelection = {}; + player_audio_track_info_v2 *audio_track_info = nullptr; + + ret = player_get_audio_track_info_v2(player_, audio_index, + &audio_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_audio_track_info_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + LOG_INFO( + "[VideoPlayer] audio track info: language[%s], channel[%d], " + "sample_rate[%d], bitrate[%d]", + audio_track_info->language, audio_track_info->channel, + audio_track_info->sample_rate, audio_track_info->bit_rate); + + trackSelection.insert( + {flutter::EncodableValue("trackType"), + flutter::EncodableValue(PLAYER_STREAM_TYPE_AUDIO)}); + trackSelection.insert({flutter::EncodableValue("trackId"), + flutter::EncodableValue(audio_index)}); + trackSelection.insert( + {flutter::EncodableValue("language"), + flutter::EncodableValue(std::string(audio_track_info->language))}); + trackSelection.insert( + {flutter::EncodableValue("channel"), + flutter::EncodableValue(audio_track_info->channel)}); + trackSelection.insert( + {flutter::EncodableValue("bitrate"), + flutter::EncodableValue(audio_track_info->bit_rate)}); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + + } else if (track_type == PLAYER_STREAM_TYPE_TEXT) { + LOG_INFO("[VideoPlayer] subtitle_count: %d", track_count); + + FuncPlayerGetSubtitleTrackInfoV2 player_get_subtitle_track_info_v2 = + reinterpret_cast( + dlsym(player_lib_handle, "player_get_subtitle_track_info_v2")); + if (!player_get_subtitle_track_info_v2) { + LOG_ERROR("[VideoPlayer] Symbol not found: %s", dlerror()); + dlclose(player_lib_handle); + return {}; + } + + for (int sub_index = 0; sub_index < track_count; sub_index++) { + flutter::EncodableMap trackSelection = {}; + player_subtitle_track_info_v2 *sub_track_info = nullptr; + + ret = player_get_subtitle_track_info_v2(player_, sub_index, + &sub_track_info); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_subtitle_track_info_v2 failed: %s", + get_error_message(ret)); + dlclose(player_lib_handle); + return {}; + } + LOG_INFO( + "[VideoPlayer] subtitle track info: language[%s], " + "subtitle_type[%d]", + sub_track_info->language, sub_track_info->subtitle_type); + + trackSelection.insert({flutter::EncodableValue("trackType"), + flutter::EncodableValue(PLAYER_STREAM_TYPE_TEXT)}); + trackSelection.insert({flutter::EncodableValue("trackId"), + flutter::EncodableValue(sub_index)}); + trackSelection.insert( + {flutter::EncodableValue("language"), + flutter::EncodableValue(std::string(sub_track_info->language))}); + trackSelection.insert( + {flutter::EncodableValue("subtitleType"), + flutter::EncodableValue(sub_track_info->subtitle_type)}); + + trackSelections.push_back(flutter::EncodableValue(trackSelection)); + } + } + + dlclose(player_lib_handle); + return trackSelections; +} + +void VideoPlayer::SetTrackSelection(int32_t track_id, int32_t track_type) { + LOG_INFO("[VideoPlayer] track_id: %d,track_type: %d", track_id, track_type); + + player_state_e state = PLAYER_STATE_NONE; + int ret = player_get_state(player_, &state); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_get_state failed: %s", + get_error_message(ret)); + return; + } + if (state == PLAYER_STATE_NONE || state == PLAYER_STATE_IDLE) { + LOG_ERROR("[VideoPlayer] Player not ready."); + return; + } + + ret = + player_select_track(player_, (player_stream_type_e)track_type, track_id); + if (ret != PLAYER_ERROR_NONE) { + LOG_ERROR("[VideoPlayer] player_select_track failed: %s", + get_error_message(ret)); + } +} + void VideoPlayer::SeekTo(int32_t position, SeekCompletedCallback callback) { LOG_INFO("[VideoPlayer] position: %d", position); diff --git a/packages/video_player_videohole/tizen/src/video_player.h b/packages/video_player_videohole/tizen/src/video_player.h index acdf97628..1fdf9038a 100644 --- a/packages/video_player_videohole/tizen/src/video_player.h +++ b/packages/video_player_videohole/tizen/src/video_player.h @@ -21,12 +21,50 @@ #include "drm_manager.h" #include "video_player_options.h" +#define MAX_STRING_NAME_LEN 255 +#define MMPLAYER_FOUR_CC_LEN 14 +#define PLAYER_LANG_NAME_SIZE 10 + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char name[MAX_STRING_NAME_LEN]; /**< name: video/audio, it maybe not exit in + some track*/ + /*dynamic infos in hls,ss,dash streams*/ + int width; /**< resolution width */ + int height; /**< resolution height */ + int bit_rate; /**< bitrate in bps */ +} player_video_track_info_v2; + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char language[PLAYER_LANG_NAME_SIZE]; /**< language info*/ + /*dynamic infos in hls,ss,dash streams*/ + int sample_rate; /**< sample rate in this track*/ + int channel; /**< channel in this track*/ + int bit_rate; /**< bitrate in this track*/ +} player_audio_track_info_v2; + +typedef struct { + char fourCC[MMPLAYER_FOUR_CC_LEN + 1]; /**< codec fourcc */ + char language[PLAYER_LANG_NAME_SIZE]; /**< language info*/ + int subtitle_type; /**< text subtitle = 0, picture subtitle = 1 */ +} player_subtitle_track_info_v2; + typedef void (*FuncEcoreWl2WindowGeometryGet)(void *window, int *x, int *y, int *width, int *height); typedef int (*FuncPlayerSetEcoreWlDisplay)(player_h player, player_display_type_e type, void *ecore_wl_window, int x, int y, int width, int height); +typedef int (*FuncPlayerGetTrackCountV2)(player_h player, + player_stream_type_e type, + int *pcount); +typedef int (*FuncPlayerGetVideoTrackInfoV2)( + player_h player, int index, player_video_track_info_v2 **track_info); +typedef int (*FuncPlayerGetAudioTrackInfoV2)( + player_h player, int index, player_audio_track_info_v2 **track_info); +typedef int (*FuncPlayerGetSubtitleTrackInfoV2)( + player_h player, int index, player_subtitle_track_info_v2 **track_info); class VideoPlayer { public: @@ -48,6 +86,8 @@ class VideoPlayer { void SetPlaybackSpeed(double speed); void SeekTo(int32_t position, SeekCompletedCallback callback); int32_t GetPosition(); + flutter::EncodableList getTrackInfo(int32_t track_type); + void SetTrackSelection(int32_t track_id, int32_t track_type); void RegisterSendPort(Dart_Port send_port) { send_port_ = send_port; } diff --git a/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc b/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc index b72ae520a..11a2962a3 100644 --- a/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc +++ b/packages/video_player_videohole/tizen/src/video_player_tizen_plugin.cc @@ -44,6 +44,9 @@ class VideoPlayerTizenPlugin : public flutter::Plugin, void SeekTo( const PositionMessage &msg, std::function reply)> result) override; + virtual ErrorOr Track(const TrackTypeMessage &msg) override; + std::optional SetTrackSelection( + const SelectedTracksMessage &msg) override; std::optional Pause(const PlayerMessage &msg) override; std::optional SetMixWithOthers( const MixWithOthersMessage &msg) override; @@ -243,6 +246,29 @@ void VideoPlayerTizenPlugin::SeekTo( player->SeekTo(msg.position(), [result]() -> void { result(std::nullopt); }); } +ErrorOr VideoPlayerTizenPlugin::Track( + const TrackTypeMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + + if (!player) { + return FlutterError("Invalid argument", "Player not found."); + } + + TrackMessage result(msg.player_id(), player->getTrackInfo(msg.track_type())); + return result; +} + +std::optional VideoPlayerTizenPlugin::SetTrackSelection( + const SelectedTracksMessage &msg) { + VideoPlayer *player = FindPlayerById(msg.player_id()); + if (!player) { + return FlutterError("Invalid argument", "Player not found."); + } + player->SetTrackSelection(msg.track_id(), msg.track_type()); + + return std::nullopt; +} + std::optional VideoPlayerTizenPlugin::SetDisplayGeometry( const GeometryMessage &msg) { VideoPlayer *player = FindPlayerById(msg.player_id());