Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
67 changes: 67 additions & 0 deletions lib/src/chewie_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:chewie/src/chewie_progress_colors.dart';
import 'package:chewie/src/models/option_item.dart';
import 'package:chewie/src/models/options_translation.dart';
import 'package:chewie/src/models/subtitle_model.dart';
import 'package:chewie/src/models/subtitle_track.dart';
import 'package:chewie/src/notifiers/player_notifier.dart';
import 'package:chewie/src/player_with_controls.dart';
import 'package:flutter/foundation.dart';
Expand Down Expand Up @@ -316,6 +317,9 @@ class ChewieController extends ChangeNotifier {
this.subtitle,
this.showSubtitles = false,
this.subtitleBuilder,
this.subtitleTracks = const <SubtitleTrack>[],
this.activeSubtitleTrackId,
this.onSubtitleTrackChanged,
this.customControls,
this.errorBuilder,
this.bufferingBuilder,
Expand Down Expand Up @@ -369,6 +373,9 @@ class ChewieController extends ChangeNotifier {
Subtitles? subtitle,
bool? showSubtitles,
Widget Function(BuildContext, dynamic)? subtitleBuilder,
List<SubtitleTrack>? subtitleTracks,
Object? activeSubtitleTrackId,
void Function(SubtitleTrack? track)? onSubtitleTrackChanged,
Widget? customControls,
WidgetBuilder? bufferingBuilder,
Widget Function(BuildContext, String)? errorBuilder,
Expand Down Expand Up @@ -431,6 +438,11 @@ class ChewieController extends ChangeNotifier {
showSubtitles: showSubtitles ?? this.showSubtitles,
subtitle: subtitle ?? this.subtitle,
subtitleBuilder: subtitleBuilder ?? this.subtitleBuilder,
subtitleTracks: subtitleTracks ?? this.subtitleTracks,
activeSubtitleTrackId:
activeSubtitleTrackId ?? this.activeSubtitleTrackId,
onSubtitleTrackChanged:
onSubtitleTrackChanged ?? this.onSubtitleTrackChanged,
customControls: customControls ?? this.customControls,
errorBuilder: errorBuilder ?? this.errorBuilder,
bufferingBuilder: bufferingBuilder ?? this.bufferingBuilder,
Expand Down Expand Up @@ -502,6 +514,28 @@ class ChewieController extends ChangeNotifier {
/// begins playing. If set to `false`, subtitles will be hidden by default.
bool showSubtitles;

/// Selectable subtitle tracks shown in the options menu.
///
/// Source-agnostic: the host populates this (e.g. from an HLS manifest) and
/// reacts to selection via [onSubtitleTrackChanged]. May change after the
/// video loads — use [setSubtitleTracks] so the controls rebuild.
List<SubtitleTrack> subtitleTracks;

/// Id of the currently selected track in [subtitleTracks], or `null` when
/// subtitles are off.
Object? activeSubtitleTrackId;

/// Called when the user picks a track from the menu (or `null` for "Off").
final void Function(SubtitleTrack? track)? onSubtitleTrackChanged;

/// The text of the cue currently on screen, for streaming sources that emit
/// cues over time (e.g. HLS) rather than as a static [subtitle] list. Push
/// updates with [setLiveSubtitle]; the controls render it when non-null.
final ValueNotifier<String?> liveSubtitle = ValueNotifier<String?>(null);

/// Whether any selectable subtitle tracks are available.
bool get hasSubtitleTracks => subtitleTracks.isNotEmpty;

/// The controller for the video you want to play
final VideoPlayerController videoPlayerController;

Expand Down Expand Up @@ -718,6 +752,39 @@ class ChewieController extends ChangeNotifier {
void setSubtitle(List<Subtitle> newSubtitle) {
subtitle = Subtitles(newSubtitle);
}

/// Replaces the selectable [subtitleTracks] and rebuilds the controls.
///
/// Use when tracks become known only after the media loads (e.g. once an
/// HLS manifest is parsed).
void setSubtitleTracks(List<SubtitleTrack> tracks) {
subtitleTracks = tracks;
notifyListeners();
}

/// Selects [track] (or `null` to turn subtitles off), updating
/// [activeSubtitleTrackId], notifying [onSubtitleTrackChanged] and clearing
/// any on-screen [liveSubtitle] when turned off.
void selectSubtitleTrack(SubtitleTrack? track) {
activeSubtitleTrackId = track?.id;
if (track == null) {
liveSubtitle.value = null;
}
onSubtitleTrackChanged?.call(track);
notifyListeners();
}

/// Pushes the current cue text for streaming subtitle sources. Pass `null`
/// when no cue is showing.
void setLiveSubtitle(String? text) {
liveSubtitle.value = text;
}

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

class ChewieControllerProvider extends InheritedWidget {
Expand Down
122 changes: 103 additions & 19 deletions lib/src/material/material_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import 'package:chewie/src/helpers/utils.dart';
import 'package:chewie/src/material/material_progress_bar.dart';
import 'package:chewie/src/material/widgets/options_dialog.dart';
import 'package:chewie/src/material/widgets/playback_speed_dialog.dart';
import 'package:chewie/src/material/widgets/subtitle_track_dialog.dart';
import 'package:chewie/src/models/option_item.dart';
import 'package:chewie/src/models/subtitle_model.dart';
import 'package:chewie/src/models/subtitle_track.dart';
import 'package:chewie/src/notifiers/index.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -85,17 +87,13 @@ class _MaterialControlsState extends State<MaterialControls>
Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
if (_subtitleOn)
Transform.translate(
offset: Offset(
0.0,
notifier.hideStuff ? barHeight * 0.8 : 0.0,
),
child: _buildSubtitles(
context,
chewieController.subtitle!,
),
Transform.translate(
offset: Offset(
0.0,
notifier.hideStuff ? barHeight * 0.8 : 0.0,
),
child: _buildSubtitleLayer(context),
),
_buildBottomBar(context),
],
),
Expand Down Expand Up @@ -164,6 +162,17 @@ class _MaterialControlsState extends State<MaterialControls>
chewieController.optionsTranslation?.playbackSpeedButtonText ??
'Playback speed',
),
if (chewieController.hasSubtitleTracks)
OptionItem(
onTap: (context) async {
Navigator.pop(context);
await _onSubtitleTrackButtonTap();
},
iconData: Icons.subtitles_outlined,
title:
chewieController.optionsTranslation?.subtitlesButtonText ??
'Subtitles',
),
];

if (chewieController.additionalOptions != null &&
Expand Down Expand Up @@ -208,6 +217,27 @@ class _MaterialControlsState extends State<MaterialControls>
);
}

/// Picks the right subtitle source: a streaming [ChewieController.liveSubtitle]
/// (for sources like HLS that emit cues over time) when present, otherwise
/// the static [ChewieController.subtitle] cue list.
Widget _buildSubtitleLayer(BuildContext context) {
if (!_subtitleOn) return const SizedBox();

final usesLiveCues =
chewieController.hasSubtitleTracks || chewieController.subtitle == null;
if (usesLiveCues) {
return ValueListenableBuilder<String?>(
valueListenable: chewieController.liveSubtitle,
builder: (context, text, _) {
if (text == null || text.isEmpty) return const SizedBox();
return _subtitleBox(context, text);
},
);
}

return _buildSubtitles(context, chewieController.subtitle!);
}

Widget _buildSubtitles(BuildContext context, Subtitles subtitles) {
if (!_subtitleOn) {
return const SizedBox();
Expand All @@ -217,11 +247,12 @@ class _MaterialControlsState extends State<MaterialControls>
return const SizedBox();
}

return _subtitleBox(context, currentSubtitle.first!.text);
}

Widget _subtitleBox(BuildContext context, dynamic text) {
if (chewieController.subtitleBuilder != null) {
return chewieController.subtitleBuilder!(
context,
currentSubtitle.first!.text,
);
return chewieController.subtitleBuilder!(context, text);
}

return Padding(
Expand All @@ -233,7 +264,7 @@ class _MaterialControlsState extends State<MaterialControls>
borderRadius: BorderRadius.circular(10.0),
),
child: Text(
currentSubtitle.first!.text.toString(),
text.toString(),
style: const TextStyle(fontSize: 18),
textAlign: TextAlign.center,
),
Expand Down Expand Up @@ -467,8 +498,10 @@ class _MaterialControlsState extends State<MaterialControls>
}

Widget _buildSubtitleToggle() {
//if don't have subtitle hiden button
if (chewieController.subtitle?.isEmpty ?? true) {
// Hide the button when there's nothing to toggle: neither a static cue
// list nor selectable tracks.
final hasStaticSubtitle = chewieController.subtitle?.isNotEmpty ?? false;
if (!hasStaticSubtitle && !chewieController.hasSubtitleTracks) {
return const SizedBox();
}
return GestureDetector(
Expand All @@ -488,11 +521,61 @@ class _MaterialControlsState extends State<MaterialControls>
}

void _onSubtitleTap() {
final controller = chewieController;
// With selectable tracks, toggling also drives the track selection so the
// host can start/stop producing cues.
if (controller.hasSubtitleTracks) {
if (_subtitleOn) {
controller.selectSubtitleTrack(null);
setState(() => _subtitleOn = false);
} else {
controller.selectSubtitleTrack(_activeTrackOrDefault());
setState(() => _subtitleOn = true);
}
return;
}
setState(() {
_subtitleOn = !_subtitleOn;
});
}

SubtitleTrack? _activeTrackOrDefault() {
final tracks = chewieController.subtitleTracks;
if (tracks.isEmpty) return null;
final activeId = chewieController.activeSubtitleTrackId;
for (final track in tracks) {
if (track.id == activeId) return track;
}
return tracks.first;
}

Future<void> _onSubtitleTrackButtonTap() async {
_hideTimer?.cancel();

final choice = await showModalBottomSheet<SubtitleTrackChoice>(
context: context,
isScrollControlled: true,
useRootNavigator: chewieController.useRootNavigator,
builder: (context) => SubtitleTrackDialog(
tracks: chewieController.subtitleTracks,
selectedId: chewieController.activeSubtitleTrackId,
offLabel:
chewieController.optionsTranslation?.subtitlesButtonText != null
? '${chewieController.optionsTranslation!.subtitlesButtonText} — off'
: 'Off',
),
);

if (choice != null) {
chewieController.selectSubtitleTrack(choice.track);
setState(() => _subtitleOn = choice.track != null);
}

if (_latestValue.isPlaying) {
_startHideTimer();
}
}

void _cancelAndRestartTimer() {
_hideTimer?.cancel();
_startHideTimer();
Expand All @@ -505,8 +588,9 @@ class _MaterialControlsState extends State<MaterialControls>

Future<void> _initialize() async {
_subtitleOn =
chewieController.showSubtitles &&
(chewieController.subtitle?.isNotEmpty ?? false);
(chewieController.showSubtitles &&
(chewieController.subtitle?.isNotEmpty ?? false)) ||
chewieController.activeSubtitleTrackId != null;
controller.addListener(_updateState);

_updateState();
Expand Down
Loading