Skip to content
Prev Previous commit
Next Next commit
Move event stream to the instance class
  • Loading branch information
stuartmorgan-g committed Aug 7, 2025
commit 2f3a4df41bc0da46d0b20432bbdd9d0ed0ebebff
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {

@override
Future<void> dispose(int playerId) async {
final _PlayerInstance? player = _players.remove(playerId);
await _api.dispose(playerId);
_players.remove(playerId);
await player?.dispose();
}

@override
Expand Down Expand Up @@ -132,7 +133,13 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {
),
VideoViewType.platformView => const _VideoPlayerPlatformViewState(),
};
return _PlayerInstance(_playerProvider(playerId), viewState);
final String eventChannelName =
'flutter.io/videoPlayer/videoEvents$playerId';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the magic string a constant outside of this method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted, and added a comment about the value it has to match on the other side.

return _PlayerInstance(
_playerProvider(playerId),
viewState,
eventChannelName: eventChannelName,
);
});
}

Expand Down Expand Up @@ -176,41 +183,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {

@override
Stream<VideoEvent> videoEventsFor(int playerId) {
return _eventChannelFor(playerId).receiveBroadcastStream().map((
dynamic event,
) {
final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
return switch (map['event']) {
'initialized' => VideoEvent(
eventType: VideoEventType.initialized,
duration: Duration(milliseconds: map['duration'] as int),
size: Size(
(map['width'] as num?)?.toDouble() ?? 0.0,
(map['height'] as num?)?.toDouble() ?? 0.0,
),
rotationCorrection: map['rotationCorrection'] as int? ?? 0,
),
'completed' => VideoEvent(eventType: VideoEventType.completed),
'bufferingUpdate' => VideoEvent(
eventType: VideoEventType.bufferingUpdate,
buffered: <DurationRange>[
DurationRange(
Duration.zero,
Duration(milliseconds: map['position'] as int),
),
],
),
'bufferingStart' => VideoEvent(
eventType: VideoEventType.bufferingStart,
),
'bufferingEnd' => VideoEvent(eventType: VideoEventType.bufferingEnd),
'isPlayingStateUpdate' => VideoEvent(
eventType: VideoEventType.isPlayingStateUpdate,
isPlaying: map['isPlaying'] as bool,
),
_ => VideoEvent(eventType: VideoEventType.unknown),
};
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This switch is moved almost unchanged to the player instance class below.

return _playerWith(id: playerId).videoEvents();
}

@override
Expand All @@ -236,10 +209,6 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {
return _api.setMixWithOthers(mixWithOthers);
}

EventChannel _eventChannelFor(int playerId) {
return EventChannel('flutter.io/videoPlayer/videoEvents$playerId');
}

_PlayerInstance _playerWith({required int id}) {
final _PlayerInstance? player = _players[id];
return player ?? (throw StateError('No active player with ID $id.'));
Expand Down Expand Up @@ -274,9 +243,25 @@ PlatformVideoViewType _platformVideoViewTypeFromVideoViewType(
class _PlayerInstance {
/// Creates a new instance of [_PlayerInstance] corresponding to the given
/// API instance.
_PlayerInstance(this._api, this.viewState);
_PlayerInstance(
this._api,
this.viewState, {
required String eventChannelName,
}) {
_eventChannel = EventChannel(eventChannelName);
_eventSubscription = _eventChannel.receiveBroadcastStream().listen(
_onStreamEvent,
onError: (Object e) {
_eventStreamController.addError(e);
},
);
}

final VideoPlayerInstanceApi _api;
late final EventChannel _eventChannel;
final StreamController<VideoEvent> _eventStreamController =
StreamController<VideoEvent>.broadcast();
late final StreamSubscription<dynamic> _eventSubscription;

final _VideoPlayerViewState viewState;

Expand Down Expand Up @@ -307,6 +292,49 @@ class _PlayerInstance {
Future<int> getPosition() {
return _api.getPosition();
}

Stream<VideoEvent> videoEvents() {
return _eventStreamController.stream;
}

Future<void> dispose() async {
await _eventSubscription.cancel();
}

void _onStreamEvent(dynamic event) {
final Map<dynamic, dynamic> map = event as Map<dynamic, dynamic>;
_eventStreamController.add(switch (map['event']) {
'initialized' => VideoEvent(
eventType: VideoEventType.initialized,
duration: Duration(milliseconds: map['duration'] as int),
size: Size(
(map['width'] as num?)?.toDouble() ?? 0.0,
(map['height'] as num?)?.toDouble() ?? 0.0,
),
rotationCorrection: map['rotationCorrection'] as int? ?? 0,
),
'completed' => VideoEvent(eventType: VideoEventType.completed),
'bufferingUpdate' => VideoEvent(
eventType: VideoEventType.bufferingUpdate,
buffered: _bufferRangeForPosition(map['position'] as int),
),
Comment on lines +346 to +349

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This bufferingUpdate case appears to be unreachable. With the refactoring in this PR, the logic for sending buffer updates has been moved from the native Java code to the getPosition method in Dart. The native side should no longer be sending 'bufferingUpdate' events, making this case dead code. It can be safely removed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct; the Java code still sends this when the player enters STATE_BUFFERING.

'bufferingStart' => VideoEvent(eventType: VideoEventType.bufferingStart),
'bufferingEnd' => VideoEvent(eventType: VideoEventType.bufferingEnd),
'isPlayingStateUpdate' => VideoEvent(
eventType: VideoEventType.isPlayingStateUpdate,
isPlaying: map['isPlaying'] as bool,
),
_ => VideoEvent(eventType: VideoEventType.unknown),
});
}

// Turns a single buffer position, which is what ExoPlayer reports, into the
// DurationRange array expected by [VideoEventType.bufferingUpdate].
List<DurationRange> _bufferRangeForPosition(int milliseconds) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was extracted from the switch to allow it to be used in _updateBufferingState as well.

return <DurationRange>[
DurationRange(Duration.zero, Duration(milliseconds: milliseconds)),
];
}
}

/// Base class representing the state of a video player view.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,12 +545,8 @@ void main() {
});

test('videoEventsFor', () async {
final (
AndroidVideoPlayer player,
MockAndroidVideoPlayerApi api,
_,
) = setUpMockPlayer(playerId: 1);
const String mockChannel = 'flutter.io/videoPlayer/videoEvents123';
const int playerId = 1;
const String mockChannel = 'flutter.io/videoPlayer/videoEvents$playerId';
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMessageHandler(mockChannel, (ByteData? message) async {
final MethodCall methodCall = const StandardMethodCodec()
Expand Down Expand Up @@ -669,8 +665,17 @@ void main() {
fail('Expected listen or cancel');
}
});

// Creating the player triggers the stream listener, so that must be done
// after setting up the mock native handler above.
final (
AndroidVideoPlayer player,
MockAndroidVideoPlayerApi api,
_,
) = setUpMockPlayer(playerId: playerId);

expect(
player.videoEventsFor(123),
player.videoEventsFor(playerId),
emitsInOrder(<dynamic>[
VideoEvent(
eventType: VideoEventType.initialized,
Expand Down