Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[video_player_videohole] Implement video, audio and text track selections. #607

Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions packages/video_player_videohole/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:video_player_videohole/video_player.dart';
import 'package:video_player_videohole/video_player_platform_interface.dart';
swift-kim marked this conversation as resolved.
Show resolved Hide resolved

void main() {
runApp(
Expand All @@ -37,6 +38,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 Selections'),
],
),
),
Expand All @@ -47,6 +49,7 @@ class _App extends StatelessWidget {
_DashRomoteVideo(),
_DrmRemoteVideo(),
_DrmRemoteVideo2(),
_TrackSelectionTest(),
],
),
),
Expand Down Expand Up @@ -370,6 +373,67 @@ class _DrmRemoteVideoState2 extends State<_DrmRemoteVideo2> {
}
}

class _TrackSelectionTest extends StatefulWidget {
@override
State<_TrackSelectionTest> createState() => _TrackSelectionTestState2();
}

class _TrackSelectionTestState2 extends State<_TrackSelectionTest> {
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: <Widget>[
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: <Widget>[
VideoPlayer(_controller),
ClosedCaption(text: _controller.value.caption.text),
_ControlsOverlay(controller: _controller),
VideoProgressIndicator(_controller, allowScrubbing: true),
],
),
),
),
_GetTrackSelectionButton(controller: _controller),
],
),
);
}
}

class _ControlsOverlay extends StatelessWidget {
const _ControlsOverlay({required this.controller});

Expand Down Expand Up @@ -485,3 +549,124 @@ class _ControlsOverlay extends StatelessWidget {
);
}
}

class _GetTrackSelectionButton extends StatelessWidget {
const _GetTrackSelectionButton({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 Track Selection'),
onPressed: () async {
final List<TrackSelection>? tracks =
await controller.trackSelections;
if (tracks == null) {
return;
}
// ignore: use_build_context_synchronously
await showDialog(
context: context,
builder: (_) => _TrackSelectionDialog(
controller: controller,
videoTrackSelections: tracks
.where((TrackSelection track) =>
track.trackType == TrackSelectionType.video)
.toList(),
audioTrackSelections: tracks
.where((TrackSelection track) =>
track.trackType == TrackSelectionType.audio)
.toList(),
textTrackSelections: tracks
.where((TrackSelection track) =>
track.trackType == TrackSelectionType.text)
.toList(),
),
);
}),
);
}
}

class _TrackSelectionDialog extends StatelessWidget {
const _TrackSelectionDialog({
required this.controller,
required this.videoTrackSelections,
required this.audioTrackSelections,
required this.textTrackSelections,
});

final VideoPlayerController controller;
final List<TrackSelection> videoTrackSelections;
final List<TrackSelection> audioTrackSelections;
final List<TrackSelection> textTrackSelections;
swift-kim marked this conversation as resolved.
Show resolved Hide resolved

@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: AlertDialog(
titlePadding: EdgeInsets.zero,
contentPadding: EdgeInsets.zero,
title: TabBar(
labelColor: Colors.black,
tabs: <Widget>[
if (videoTrackSelections.isNotEmpty) const Tab(text: 'Video'),
if (audioTrackSelections.isNotEmpty) const Tab(text: 'Audio'),
if (textTrackSelections.isNotEmpty) const Tab(text: 'Text'),
],
),
content: SizedBox(
height: 200,
width: 200,
child: TabBarView(
children: <Widget>[
if (videoTrackSelections.isNotEmpty)
ListView.builder(
itemCount: videoTrackSelections.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
'${videoTrackSelections[index].width}x${videoTrackSelections[index].height},${(videoTrackSelections[index].bitrate! / 1000000).toStringAsFixed(2)}Mbps'),
onTap: () {
controller
.setTrackSelection(videoTrackSelections[index]);
},
);
}),
if (audioTrackSelections.isNotEmpty)
ListView.builder(
itemCount: audioTrackSelections.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
'language:${audioTrackSelections[index].language}'),
onTap: () {
controller
.setTrackSelection(audioTrackSelections[index]);
},
);
}),
if (textTrackSelections.isNotEmpty)
ListView.builder(
itemCount: textTrackSelections.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(
'language:${textTrackSelections[index].language}'),
onTap: () {
controller
.setTrackSelection(textTrackSelections[index]);
},
);
}),
],
),
),
),
);
}
}
119 changes: 118 additions & 1 deletion packages/video_player_videohole/lib/src/messages.g.dart
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,64 @@ class PlaybackSpeedMessage {
}
}

class TrackMessage {
TrackMessage({
required this.playerId,
required this.trackSelections,
});

int playerId;

List<Map<Object?, Object?>?> trackSelections;

Object encode() {
return <Object?>[
playerId,
trackSelections,
];
}

static TrackMessage decode(Object result) {
result as List<Object?>;
return TrackMessage(
playerId: result[0]! as int,
trackSelections:
(result[1] as List<Object?>?)!.cast<Map<Object?, Object?>?>(),
);
}
}

class SelectedTracksMessage {
SelectedTracksMessage({
required this.playerId,
required this.trackId,
required this.trackType,
});

int playerId;

int trackId;

int trackType;

Object encode() {
return <Object?>[
playerId,
trackId,
trackType,
];
}

static SelectedTracksMessage decode(Object result) {
result as List<Object?>;
return SelectedTracksMessage(
playerId: result[0]! as int,
trackId: result[1]! as int,
trackType: result[2]! as int,
);
}
}

class PositionMessage {
PositionMessage({
required this.playerId,
Expand Down Expand Up @@ -268,9 +326,15 @@ 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 VolumeMessage) {
buffer.putUint8(137);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
Expand All @@ -294,6 +358,10 @@ 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 VolumeMessage.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
Expand Down Expand Up @@ -518,6 +586,55 @@ class VideoPlayerVideoholeApi {
}
}

Future<TrackMessage> trackSelections(PlayerMessage arg_msg) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.VideoPlayerVideoholeApi.trackSelections', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_msg]) as List<Object?>?;
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<void> setTrackSelection(SelectedTracksMessage arg_msg) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.VideoPlayerVideoholeApi.setTrackSelection', codec,
binaryMessenger: _binaryMessenger);
final List<Object?>? replyList =
await channel.send(<Object?>[arg_msg]) as List<Object?>?;
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<void> pause(PlayerMessage arg_msg) async {
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.VideoPlayerVideoholeApi.pause', codec,
Expand Down
Loading