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

Add PiP support on iOS - Events and Page #75

Merged
merged 19 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 17 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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Added
Support for Picture-in-Picture (PiP) playback on iOS
- `PictureInPictureEnter` which is emitted when the player view is about to enter Picture-in-Picture mode
- `PictureInPictureExit` which is emitted when the player view is about to exit Picture-in-Picture mode
- `PictureInPictureEntered` which is emitted when the player view finishes entering Picture-in-Picture mode
- `PictureInPictureExited` which is emitted when the player view finishes exiting Picture-in-Picture mode
- `PictureInPicture` example page to test PiP and show how it can be integrated and used

## [0.3.0] - 2023-11-27
### Added
- Support for background playback on iOS
Expand Down
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:bitmovin_player_example/pages/drm_playback.dart';
import 'package:bitmovin_player_example/pages/event_subscription.dart';
import 'package:bitmovin_player_example/pages/fullscreen_handling.dart';
import 'package:bitmovin_player_example/pages/home.dart';
import 'package:bitmovin_player_example/pages/picture_in_picture.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

Expand Down Expand Up @@ -43,6 +44,7 @@ class _MyAppState extends State<MyApp> {
FullscreenHandling.routeName: (_) => const FullscreenHandling(),
Casting.routeName: (_) => const Casting(),
BackgroundPlayback.routeName: (_) => const BackgroundPlayback(),
PictureInPicture.routeName: (_) => const PictureInPicture(),
},
home: Scaffold(
body: Home(),
Expand Down
7 changes: 6 additions & 1 deletion example/lib/pages/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:bitmovin_player_example/pages/custom_html_ui.dart';
import 'package:bitmovin_player_example/pages/drm_playback.dart';
import 'package:bitmovin_player_example/pages/event_subscription.dart';
import 'package:bitmovin_player_example/pages/fullscreen_handling.dart';
import 'package:bitmovin_player_example/pages/picture_in_picture.dart';
import 'package:flutter/material.dart';

List<_Sample> _samples = [];
Expand All @@ -26,7 +27,11 @@ void buildSamples() {
];

if (Platform.isIOS) {
_samples.add(_Sample('Background Playback', BackgroundPlayback.routeName));
_samples
..add(_Sample('Background Playback', BackgroundPlayback.routeName))
..add(
_Sample('Picture-in-Picture', PictureInPicture.routeName),
);
123mpozzi marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down
110 changes: 110 additions & 0 deletions example/lib/pages/picture_in_picture.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import 'package:audio_session/audio_session.dart';
import 'package:bitmovin_player/bitmovin_player.dart';
import 'package:bitmovin_player_example/env/env.dart';
import 'package:bitmovin_player_example/events.dart';
import 'package:flutter/material.dart';
import 'package:logger/logger.dart';

class PictureInPicture extends StatefulWidget {
const PictureInPicture({super.key});
static String routeName = 'PictureInPicture';

@override
State<PictureInPicture> createState() => _PictureInPictureState();
}

class _PictureInPictureState extends State<PictureInPicture> {
final _playerViewKey = GlobalKey<PlayerViewState>();
final _sourceConfig = const SourceConfig(
url:
'https://bitmovin-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8',
type: SourceType.hls,
);
final _player = Player(
config: const PlayerConfig(
key: Env.bitmovinPlayerLicenseKey,
remoteControlConfig: RemoteControlConfig(isCastEnabled: false),
),
);
final _logger = Logger();

final _eventsKey = GlobalKey<EventsState>();
123mpozzi marked this conversation as resolved.
Show resolved Hide resolved
void _onEvent(Event event) {
final eventName = '${event.runtimeType}';
final eventData = '$eventName ${event.toJson()}';
_logger.d(eventData);
_eventsKey.currentState?.add(eventName);
}

@override
void initState() {
setupAudioSession();
_player.loadSourceConfig(_sourceConfig);
super.initState();
}

Future<void> setupAudioSession() async {
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.music());
}

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

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Picture-in-Picture'),
),
body: Column(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: PlayerView(
player: _player,
key: _playerViewKey,
onPictureInPictureEnter: _onEvent,
onPictureInPictureEntered: _onEvent,
onPictureInPictureExit: _onEvent,
onPictureInPictureExited: _onEvent,
),
),
Row(
children: [
Container(
margin: const EdgeInsets.only(left: 10, right: 5),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue),
),
onPressed: () {},
child: const Text('Enter PiP'),
),
),
Container(
margin: const EdgeInsets.all(5),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue),
),
onPressed: () {},
child: const Text('Exit PiP'),
),
),
],
),
Expanded(
child: Container(
margin: const EdgeInsets.fromLTRB(10, 10, 10, 40),
child: Events(key: _eventsKey),
),
),
],
),
);
}
}
16 changes: 16 additions & 0 deletions ios/Classes/FlutterPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,20 @@ extension FlutterPlayerView: UserInterfaceListener {
func onFullscreenExit(_ event: FullscreenExitEvent, view: PlayerView) {
broadcast(name: event.name, data: event.toJsonFallback(), sink: eventSink)
}

func onPictureInPictureEnter(_ event: PictureInPictureEnterEvent, view: PlayerView) {
broadcast(name: event.name, data: event.toJsonFallback(), sink: eventSink)
}

func onPictureInPictureEntered(_ event: PictureInPictureEnteredEvent, view: PlayerView) {
broadcast(name: event.name, data: event.toJsonFallback(), sink: eventSink)
}

func onPictureInPictureExit(_ event: PictureInPictureExitEvent, view: PlayerView) {
broadcast(name: event.name, data: event.toJsonFallback(), sink: eventSink)
}

func onPictureInPictureExited(_ event: PictureInPictureExitedEvent, view: PlayerView) {
broadcast(name: event.name, data: event.toJsonFallback(), sink: eventSink)
}
}
4 changes: 4 additions & 0 deletions lib/bitmovin_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export 'src/api/event/player/unmuted_event.dart';
export 'src/api/event/player/warning_event.dart';
export 'src/api/event/player_view/fullscreen_enter_event.dart';
export 'src/api/event/player_view/fullscreen_exit_event.dart';
export 'src/api/event/player_view/picture_in_picture_enter_event.dart';
export 'src/api/event/player_view/picture_in_picture_entered_event.dart';
export 'src/api/event/player_view/picture_in_picture_exit_event.dart';
export 'src/api/event/player_view/picture_in_picture_exited_event.dart';
export 'src/api/event/source/source_added_event.dart';
export 'src/api/event/source/source_error_event.dart';
export 'src/api/event/source/source_info_event.dart';
Expand Down
21 changes: 21 additions & 0 deletions lib/src/api/event/player_view/picture_in_picture_enter_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:bitmovin_player/bitmovin_player.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'picture_in_picture_enter_event.g.dart';

/// Is called when the [PlayerView] is about to enter Picture-in-Picture mode.
@JsonSerializable(explicitToJson: true)
class PictureInPictureEnterEvent extends Event with EquatableMixin {
const PictureInPictureEnterEvent({required super.timestamp});

factory PictureInPictureEnterEvent.fromJson(Map<String, dynamic> json) {
return _$PictureInPictureEnterEventFromJson(json);
}

@override
List<Object?> get props => [timestamp];

@override
Map<String, dynamic> toJson() => _$PictureInPictureEnterEventToJson(this);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:bitmovin_player/bitmovin_player.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'picture_in_picture_entered_event.g.dart';

/// Is called when the [PlayerView] finished entering Picture in Picture mode.
///
/// Only available on iOS.
@JsonSerializable(explicitToJson: true)
class PictureInPictureEnteredEvent extends Event with EquatableMixin {
const PictureInPictureEnteredEvent({required super.timestamp});

factory PictureInPictureEnteredEvent.fromJson(Map<String, dynamic> json) {
return _$PictureInPictureEnteredEventFromJson(json);
}

@override
List<Object?> get props => [timestamp];

@override
Map<String, dynamic> toJson() => _$PictureInPictureEnteredEventToJson(this);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions lib/src/api/event/player_view/picture_in_picture_exit_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:bitmovin_player/bitmovin_player.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'picture_in_picture_exit_event.g.dart';

/// Is called when the [PlayerView] is about to exit Picture in Picture mode.
@JsonSerializable(explicitToJson: true)
class PictureInPictureExitEvent extends Event with EquatableMixin {
const PictureInPictureExitEvent({required super.timestamp});

factory PictureInPictureExitEvent.fromJson(Map<String, dynamic> json) {
return _$PictureInPictureExitEventFromJson(json);
}

@override
List<Object?> get props => [timestamp];

@override
Map<String, dynamic> toJson() => _$PictureInPictureExitEventToJson(this);
}
19 changes: 19 additions & 0 deletions lib/src/api/event/player_view/picture_in_picture_exit_event.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions lib/src/api/event/player_view/picture_in_picture_exited_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:bitmovin_player/bitmovin_player.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'picture_in_picture_exited_event.g.dart';

/// Is called when the [PlayerView] finished exiting Picture in Picture mode.
///
/// Only available on iOS.
@JsonSerializable(explicitToJson: true)
class PictureInPictureExitedEvent extends Event with EquatableMixin {
const PictureInPictureExitedEvent({required super.timestamp});

factory PictureInPictureExitedEvent.fromJson(Map<String, dynamic> json) {
return _$PictureInPictureExitedEventFromJson(json);
}

@override
List<Object?> get props => [timestamp];

@override
Map<String, dynamic> toJson() => _$PictureInPictureExitedEventToJson(this);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions lib/src/player_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class PlayerView extends StatefulWidget with PlayerViewEventHandler {
this.fullscreenHandler,
this.onFullscreenEnter,
this.onFullscreenExit,
this.onPictureInPictureEnter,
this.onPictureInPictureEntered,
this.onPictureInPictureExit,
this.onPictureInPictureExited,
});

/// The [Player] instance that is attached to this view.
Expand All @@ -42,6 +46,18 @@ class PlayerView extends StatefulWidget with PlayerViewEventHandler {
@override
final void Function(FullscreenExitEvent)? onFullscreenExit;

@override
final void Function(PictureInPictureEnterEvent)? onPictureInPictureEnter;

@override
final void Function(PictureInPictureEnteredEvent)? onPictureInPictureEntered;

@override
final void Function(PictureInPictureExitEvent)? onPictureInPictureExit;

@override
final void Function(PictureInPictureExitedEvent)? onPictureInPictureExited;

@override
State<StatefulWidget> createState() => PlayerViewState();
}
Expand Down
Loading