Skip to content

Commit

Permalink
Merge pull request #74 from bitmovin/feature/support-airplay
Browse files Browse the repository at this point in the history
Add AirPlay support
  • Loading branch information
123mpozzi authored Nov 27, 2023
2 parents 1957fd0 + 3079b75 commit 1123e6c
Show file tree
Hide file tree
Showing 19 changed files with 198 additions and 2 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
### Added
- `isBackgroundPlaybackEnabled` to `PlaybackConfig`. For now this is only supported on iOS.
- `BackgroundPlayback` example to the example Application
- Support for AirPlay on iOS
- `Player.isAirPlayActive` to indicate whether media is being played externally using AirPlay.
- `Player.isAirPlayAvailable` to indicate whether AirPlay is available.
- `Player.showAirPlayTargetPicker` to display the AirPlay playback target picker.
- `RemoteControlConfig.isAirPlayEnabled` to control whether AirPlay should be possible.
- `AirPlayAvailableEvent` which is emitted when AirPlay is available.
- `AirPlayChangedEvent` which is emitted when AirPlay playback starts or stops.

## [0.2.0] - 2023-11-06
### Added
Expand Down
2 changes: 1 addition & 1 deletion example/lib/pages/casting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class _CastingState extends State<Casting> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Basic Playback'),
title: const Text('Casting'),
),
body: FutureBuilder(future: _playerState, builder: buildPlayer),
);
Expand Down
4 changes: 3 additions & 1 deletion example/lib/pages/event_subscription.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ class _EventSubscriptionState extends State<EventSubscription> {
..onSubtitleRemoved = _onEvent
..onSubtitleChanged = _onEvent
..onCueEnter = _onEvent
..onCueExit = _onEvent;
..onCueExit = _onEvent
..onAirPlayAvailable = _onEvent
..onAirPlayChanged = _onEvent;
}

@override
Expand Down
11 changes: 11 additions & 0 deletions ios/Classes/Event+JSON.swift
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,17 @@ extension CastWaitingForDeviceEvent {
}
}

extension AirPlayChangedEvent {
func toJSON() -> [String: Any] {
[
"event": name,
"timestamp": Int(timestamp),
"isAirPlayActive": isAirPlayActive,
"time": time
]
}
}

extension Double {
var jsonValue: Any {
switch self {
Expand Down
14 changes: 14 additions & 0 deletions ios/Classes/FlutterPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ private extension FlutterPlayer {
player.castVideo()
case (Methods.castStop, .empty):
player.castStop()
case (Methods.isAirPlayActive, .empty):
return player.isAirPlayActive
case (Methods.isAirPlayAvailable, .empty):
return player.isAirPlayAvailable
case (Methods.showAirPlayTargetPicker, .empty):
player.showAirPlayTargetPicker()
default:
throw BitmovinError.unknownMethod(call.method)
}
Expand Down Expand Up @@ -350,4 +356,12 @@ extension FlutterPlayer: PlayerListener {
func onCastTimeUpdated(_ event: CastTimeUpdatedEvent, player: Player) {
broadcast(name: event.name, data: event.toJsonFallback(), sink: eventSink)
}

func onAirPlayAvailable(_ event: AirPlayAvailableEvent, player: Player) {
broadcast(name: event.name, data: event.toJsonFallback(), sink: eventSink)
}

func onAirPlayChanged(_ event: AirPlayChangedEvent, player: Player) {
broadcast(name: event.name, data: event.toJSON(), sink: eventSink)
}
}
3 changes: 3 additions & 0 deletions ios/Classes/JsonObjects.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ private enum JsonValues {
internal struct FlutterRemoteControlConfig: FlutterToNativeConvertible {
let receiverStylesheetUrl: String?
let customReceiverConfig: [String: String]
let isAirPlayEnabled: Bool
let isCastEnabled: Bool
let sendManifestRequestsWithCredentials: Bool
let sendSegmentRequestsWithCredentials: Bool
Expand All @@ -287,6 +288,7 @@ internal struct FlutterRemoteControlConfig: FlutterToNativeConvertible {
}

result.customReceiverConfig = customReceiverConfig
result.isAirPlayEnabled = isAirPlayEnabled
result.isCastEnabled = isCastEnabled
result.sendManifestRequestsWithCredentials = sendManifestRequestsWithCredentials
result.sendSegmentRequestsWithCredentials = sendSegmentRequestsWithCredentials
Expand All @@ -301,6 +303,7 @@ extension RemoteControlConfig: NativeToFlutterConvertible {
FlutterRemoteControlConfig(
receiverStylesheetUrl: receiverStylesheetUrl?.absoluteString,
customReceiverConfig: customReceiverConfig,
isAirPlayEnabled: isAirPlayEnabled,
isCastEnabled: isCastEnabled,
sendManifestRequestsWithCredentials: sendManifestRequestsWithCredentials,
sendSegmentRequestsWithCredentials: sendSegmentRequestsWithCredentials,
Expand Down
3 changes: 3 additions & 0 deletions ios/Classes/Methods.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ internal enum Methods {
static let isCasting = "isCasting"
static let castVideo = "castVideo"
static let castStop = "castStop"
static let isAirPlayActive = "isAirPlayActive"
static let isAirPlayAvailable = "isAirPlayAvailable"
static let showAirPlayTargetPicker = "showAirPlayTargetPicker"

// Player view related methods
static let destroyPlayerView = "destroyPlayerView"
Expand Down
2 changes: 2 additions & 0 deletions lib/bitmovin_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export 'src/api/event/data/cast_payload.dart';
export 'src/api/event/data/seek_position.dart';
export 'src/api/event/event.dart';
export 'src/api/event/listener/player_listener.dart';
export 'src/api/event/player/airplay_available_event.dart';
export 'src/api/event/player/airplay_changed_event.dart';
export 'src/api/event/player/cast_available_event.dart';
export 'src/api/event/player/cast_start_event.dart';
export 'src/api/event/player/cast_started_event.dart';
Expand Down
10 changes: 10 additions & 0 deletions lib/src/api/casting/remote_control_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class RemoteControlConfig extends Equatable {
const RemoteControlConfig({
this.receiverStylesheetUrl,
this.customReceiverConfig = const {},
this.isAirPlayEnabled = true,
this.isCastEnabled = true,
this.sendManifestRequestsWithCredentials = false,
this.sendSegmentRequestsWithCredentials = false,
Expand All @@ -32,6 +33,14 @@ class RemoteControlConfig extends Equatable {
/// Default value is an empty map.
final Map<String, String> customReceiverConfig;

/// Whether the AirPlay option is enabled or not. Default value is `true`.
///
/// Calling [Player.showAirPlayTargetPicker] when the value is `false` will
/// not have any effect.
///
/// Only available on iOS.
final bool isAirPlayEnabled;

/// Whether casting is enabled.
/// Default value is `true`.
///
Expand All @@ -58,6 +67,7 @@ class RemoteControlConfig extends Equatable {
List<Object?> get props => [
receiverStylesheetUrl,
customReceiverConfig,
isAirPlayEnabled,
isCastEnabled,
sendManifestRequestsWithCredentials,
sendSegmentRequestsWithCredentials,
Expand Down
2 changes: 2 additions & 0 deletions lib/src/api/casting/remote_control_config.g.dart

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

6 changes: 6 additions & 0 deletions lib/src/api/event/listener/player_listener.dart
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,10 @@ abstract class PlayerListener {

/// See [CastTimeUpdatedEvent] for details on this event.
set onCastTimeUpdated(void Function(CastTimeUpdatedEvent) func);

/// See [AirPlayAvailableEvent] for details on this event.
set onAirPlayAvailable(void Function(AirPlayAvailableEvent) func);

/// See [AirPlayChangedEvent] for details on this event.
set onAirPlayChanged(void Function(AirPlayChangedEvent) func);
}
18 changes: 18 additions & 0 deletions lib/src/api/event/player/airplay_available_event.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:bitmovin_player/src/api/event/event.dart';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';

part 'airplay_available_event.g.dart';

/// Emitted when AirPlay is available.
@JsonSerializable(explicitToJson: true)
class AirPlayAvailableEvent extends Event with EquatableMixin {
const AirPlayAvailableEvent({required super.timestamp});

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

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

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

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

part 'airplay_changed_event.g.dart';

/// Emitted when AirPlay playback starts or stops.
@JsonSerializable(explicitToJson: true)
class AirPlayChangedEvent extends Event with EquatableMixin {
const AirPlayChangedEvent({
required super.timestamp,
required this.isAirPlayActive,
required this.time,
});

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

/// Indicates whether AirPlay is active.
final bool isAirPlayActive;

// The current playback time (in seconds).
final double time;

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

@override
List<Object?> get props => [timestamp, isAirPlayActive, time];
}
22 changes: 22 additions & 0 deletions lib/src/api/event/player/airplay_changed_event.g.dart

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

15 changes: 15 additions & 0 deletions lib/src/api/player/player_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,19 @@ abstract class PlayerApi {

/// Stops casting the current video.
Future<void> castStop();

/// Returns `true` when media is played externally using AirPlay.
///
/// Only available on iOS.
Future<bool> get isAirPlayActive;

/// Returns `true` when AirPlay is available.
///
/// Only available on iOS.
Future<bool> get isAirPlayAvailable;

/// Shows the AirPlay playback target picker.
///
/// Only available on iOS.
Future<void> showAirPlayTargetPicker();
}
3 changes: 3 additions & 0 deletions lib/src/methods.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class Methods {
static const String isCasting = 'isCasting';
static const String castVideo = 'castVideo';
static const String castStop = 'castStop';
static const String isAirPlayActive = 'isAirPlayActive';
static const String isAirPlayAvailable = 'isAirPlayAvailable';
static const String showAirPlayTargetPicker = 'showAirPlayTargetPicker';

/// Player view related methods
static const String destroyPlayerView = 'destroyPlayerView';
Expand Down
11 changes: 11 additions & 0 deletions lib/src/player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,17 @@ class Player with PlayerEventHandler implements PlayerApi {

@override
Future<void> castStop() => _invokeMethod<void>(Methods.castStop);

@override
Future<bool> get isAirPlayActive => _invokeMethod(Methods.isAirPlayActive);

@override
Future<bool> get isAirPlayAvailable =>
_invokeMethod(Methods.isAirPlayAvailable);

@override
Future<void> showAirPlayTargetPicker() =>
_invokeMethod<void>(Methods.showAirPlayTargetPicker);
}

class _AnalyticsApi implements AnalyticsApi {
Expand Down
17 changes: 17 additions & 0 deletions lib/src/player_event_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ mixin PlayerEventHandler implements PlayerListener {
case 'onCastTimeUpdated':
emit(CastTimeUpdatedEvent.fromJson(data));
break;
case 'onAirPlayAvailable':
case 'onAirplayAvailable':
emit(AirPlayAvailableEvent.fromJson(data));
break;
case 'onAirPlayChanged':
emit(AirPlayChangedEvent.fromJson(data));
break;
}
}

Expand Down Expand Up @@ -329,4 +336,14 @@ mixin PlayerEventHandler implements PlayerListener {
set onCastTimeUpdated(void Function(CastTimeUpdatedEvent) func) {
_addListener(func);
}

@override
set onAirPlayAvailable(void Function(AirPlayAvailableEvent) func) {
_addListener(func);
}

@override
set onAirPlayChanged(void Function(AirPlayChangedEvent) func) {
_addListener(func);
}
}

0 comments on commit 1123e6c

Please sign in to comment.