Skip to content

Commit 2c7993f

Browse files
committed
feat(video_player_android): migrate to Kotlin and improve event handling
- Moved video event processing logic to Dart for better buffer range updates during pause - Switched Pigeon-generated code to Kotlin for improved type safety - Added Kotlin plugin and configuration to build.gradle - Implemented type-safe event channels for internal communication - Refactored event handling to use dedicated event classes instead of HashMap - Simplified buffering state management by moving logic to Dart layer
1 parent ec7d56a commit 2c7993f

File tree

23 files changed

+1064
-581
lines changed

23 files changed

+1064
-581
lines changed

packages/video_player/video_player_android/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
* Implements `getAudioTracks()` and `selectAudioTrack()` methods for Android using ExoPlayer.
44

5+
## 2.8.17
6+
7+
* Moves video event processing logic to Dart, and fixes an issue where buffer
8+
range would not be updated for a paused video.
9+
* Switches to Kotlin for Pigeon-generated code.
10+
* Adopts type-safe event channels for internal communication.
11+
512
## 2.8.16
613

714
* Updates Java compatibility version to 17 and minimum supported SDK version to Flutter 3.35/Dart 3.9.

packages/video_player/video_player_android/android/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ group = 'io.flutter.plugins.videoplayer'
22
version = '1.0-SNAPSHOT'
33

44
buildscript {
5+
ext.kotlin_version = '2.2.10'
56
repositories {
67
google()
78
mavenCentral()
89
}
910

1011
dependencies {
1112
classpath 'com.android.tools.build:gradle:8.12.1'
13+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1214
}
1315
}
1416

@@ -20,6 +22,7 @@ rootProject.allprojects {
2022
}
2123

2224
apply plugin: 'com.android.library'
25+
apply plugin: 'kotlin-android'
2326

2427
android {
2528
namespace = "io.flutter.plugins.videoplayer"
@@ -38,6 +41,13 @@ android {
3841
sourceCompatibility = JavaVersion.VERSION_17
3942
targetCompatibility = JavaVersion.VERSION_17
4043
}
44+
kotlinOptions {
45+
jvmTarget = JavaVersion.VERSION_17.toString()
46+
}
47+
48+
sourceSets {
49+
main.java.srcDirs += 'src/main/kotlin'
50+
}
4151

4252
dependencies {
4353
def exoplayer_version = "1.5.1"

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/ExoPlayerEventListener.java

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import androidx.media3.exoplayer.ExoPlayer;
1111

1212
public abstract class ExoPlayerEventListener implements Player.Listener {
13-
private boolean isBuffering = false;
1413
private boolean isInitialized = false;
1514
protected final ExoPlayer exoPlayer;
1615
protected final VideoPlayerCallbacks events;
@@ -47,47 +46,34 @@ public ExoPlayerEventListener(
4746
this.events = events;
4847
}
4948

50-
private void setBuffering(boolean buffering) {
51-
if (isBuffering == buffering) {
52-
return;
53-
}
54-
isBuffering = buffering;
55-
if (buffering) {
56-
events.onBufferingStart();
57-
} else {
58-
events.onBufferingEnd();
59-
}
60-
}
61-
6249
protected abstract void sendInitialized();
6350

6451
@Override
6552
public void onPlaybackStateChanged(final int playbackState) {
53+
PlatformPlaybackState platformState = PlatformPlaybackState.UNKNOWN;
6654
switch (playbackState) {
6755
case Player.STATE_BUFFERING:
68-
setBuffering(true);
69-
events.onBufferingUpdate(exoPlayer.getBufferedPosition());
56+
platformState = PlatformPlaybackState.BUFFERING;
7057
break;
7158
case Player.STATE_READY:
59+
platformState = PlatformPlaybackState.READY;
7260
if (!isInitialized) {
7361
isInitialized = true;
7462
sendInitialized();
7563
}
7664
break;
7765
case Player.STATE_ENDED:
78-
events.onCompleted();
66+
platformState = PlatformPlaybackState.ENDED;
7967
break;
8068
case Player.STATE_IDLE:
69+
platformState = PlatformPlaybackState.IDLE;
8170
break;
8271
}
83-
if (playbackState != Player.STATE_BUFFERING) {
84-
setBuffering(false);
85-
}
72+
events.onPlaybackStateChanged(platformState);
8673
}
8774

8875
@Override
8976
public void onPlayerError(@NonNull final PlaybackException error) {
90-
setBuffering(false);
9177
if (error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW) {
9278
// See
9379
// https://exoplayer.dev/live-streaming.html#behindlivewindowexception-and-error_code_behind_live_window

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,39 @@
44

55
package io.flutter.plugins.videoplayer;
66

7-
import io.flutter.plugin.common.EventChannel;
87
import java.util.ArrayList;
98

109
/**
11-
* And implementation of {@link EventChannel.EventSink} which can wrap an underlying sink.
10+
* A wrapper for {@link PigeonEventSink} which can queue messages.
1211
*
1312
* <p>It delivers messages immediately when downstream is available, but it queues messages before
1413
* the delegate event sink is set with setDelegate.
1514
*
1615
* <p>This class is not thread-safe. All calls must be done on the same thread or synchronized
1716
* externally.
1817
*/
19-
final class QueuingEventSink implements EventChannel.EventSink {
20-
private EventChannel.EventSink delegate;
18+
final class QueuingEventSink {
19+
private PigeonEventSink<PlatformVideoEvent> delegate;
2120
private final ArrayList<Object> eventQueue = new ArrayList<>();
2221
private boolean done = false;
2322

24-
public void setDelegate(EventChannel.EventSink delegate) {
23+
public void setDelegate(PigeonEventSink<PlatformVideoEvent> delegate) {
2524
this.delegate = delegate;
2625
maybeFlush();
2726
}
2827

29-
@Override
3028
public void endOfStream() {
3129
enqueue(new EndOfStreamEvent());
3230
maybeFlush();
3331
done = true;
3432
}
3533

36-
@Override
3734
public void error(String code, String message, Object details) {
3835
enqueue(new ErrorEvent(code, message, details));
3936
maybeFlush();
4037
}
4138

42-
@Override
43-
public void success(Object event) {
39+
public void success(PlatformVideoEvent event) {
4440
enqueue(event);
4541
maybeFlush();
4642
}
@@ -63,7 +59,7 @@ private void maybeFlush() {
6359
ErrorEvent errorEvent = (ErrorEvent) event;
6460
delegate.error(errorEvent.code, errorEvent.message, errorEvent.details);
6561
} else {
66-
delegate.success(event);
62+
delegate.success((PlatformVideoEvent) event);
6763
}
6864
}
6965
eventQueue.clear();

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
*
3131
* <p>It provides methods to control playback, adjust volume, and handle seeking.
3232
*/
33-
public abstract class VideoPlayer implements Messages.VideoPlayerInstanceApi {
33+
public abstract class VideoPlayer implements VideoPlayerInstanceApi {
3434
@NonNull protected final VideoPlayerCallbacks videoPlayerEvents;
3535
@Nullable protected final SurfaceProducer surfaceProducer;
3636
@Nullable private DisposeHandler disposeHandler;
@@ -100,35 +100,37 @@ public void pause() {
100100
}
101101

102102
@Override
103-
public void setLooping(@NonNull Boolean looping) {
103+
public void setLooping(boolean looping) {
104104
exoPlayer.setRepeatMode(looping ? REPEAT_MODE_ALL : REPEAT_MODE_OFF);
105105
}
106106

107107
@Override
108-
public void setVolume(@NonNull Double volume) {
108+
public void setVolume(double volume) {
109109
float bracketedValue = (float) Math.max(0.0, Math.min(1.0, volume));
110110
exoPlayer.setVolume(bracketedValue);
111111
}
112112

113113
@Override
114-
public void setPlaybackSpeed(@NonNull Double speed) {
114+
public void setPlaybackSpeed(double speed) {
115115
// We do not need to consider pitch and skipSilence for now as we do not handle them and
116116
// therefore never diverge from the default values.
117-
final PlaybackParameters playbackParameters = new PlaybackParameters(speed.floatValue());
117+
final PlaybackParameters playbackParameters = new PlaybackParameters((float) speed);
118118

119119
exoPlayer.setPlaybackParameters(playbackParameters);
120120
}
121121

122122
@Override
123-
public @NonNull Messages.PlaybackState getPlaybackState() {
124-
return new Messages.PlaybackState.Builder()
125-
.setPlayPosition(exoPlayer.getCurrentPosition())
126-
.setBufferPosition(exoPlayer.getBufferedPosition())
127-
.build();
123+
public long getCurrentPosition() {
124+
return exoPlayer.getCurrentPosition();
128125
}
129126

130127
@Override
131-
public void seekTo(@NonNull Long position) {
128+
public long getBufferedPosition() {
129+
return exoPlayer.getBufferedPosition();
130+
}
131+
132+
@Override
133+
public void seekTo(long position) {
132134
exoPlayer.seekTo(position);
133135
}
134136

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerCallbacks.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,7 @@
1919
public interface VideoPlayerCallbacks {
2020
void onInitialized(int width, int height, long durationInMs, int rotationCorrectionInDegrees);
2121

22-
void onBufferingStart();
23-
24-
void onBufferingUpdate(long bufferedPosition);
25-
26-
void onBufferingEnd();
27-
28-
void onCompleted();
22+
void onPlaybackStateChanged(@NonNull PlatformPlaybackState state);
2923

3024
void onError(@NonNull String code, @Nullable String message, @Nullable Object details);
3125

packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerEventCallbacks.java

Lines changed: 18 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,80 +7,51 @@
77
import androidx.annotation.NonNull;
88
import androidx.annotation.Nullable;
99
import androidx.annotation.VisibleForTesting;
10-
import io.flutter.plugin.common.EventChannel;
11-
import java.util.HashMap;
12-
import java.util.Map;
10+
import io.flutter.plugin.common.BinaryMessenger;
1311

1412
final class VideoPlayerEventCallbacks implements VideoPlayerCallbacks {
15-
private final EventChannel.EventSink eventSink;
13+
private final QueuingEventSink eventSink;
1614

17-
static VideoPlayerEventCallbacks bindTo(EventChannel eventChannel) {
15+
static VideoPlayerEventCallbacks bindTo(
16+
@NonNull BinaryMessenger binaryMessenger, @NonNull String identifier) {
1817
QueuingEventSink eventSink = new QueuingEventSink();
19-
eventChannel.setStreamHandler(
20-
new EventChannel.StreamHandler() {
18+
VideoEventsStreamHandler.Companion.register(
19+
binaryMessenger,
20+
new VideoEventsStreamHandler() {
2121
@Override
22-
public void onListen(Object arguments, EventChannel.EventSink events) {
22+
public void onListen(
23+
Object arguments, @NonNull PigeonEventSink<PlatformVideoEvent> events) {
2324
eventSink.setDelegate(events);
2425
}
2526

2627
@Override
2728
public void onCancel(Object arguments) {
2829
eventSink.setDelegate(null);
2930
}
30-
});
31+
},
32+
identifier);
3133
return VideoPlayerEventCallbacks.withSink(eventSink);
3234
}
3335

3436
@VisibleForTesting
35-
static VideoPlayerEventCallbacks withSink(EventChannel.EventSink eventSink) {
37+
static VideoPlayerEventCallbacks withSink(QueuingEventSink eventSink) {
3638
return new VideoPlayerEventCallbacks(eventSink);
3739
}
3840

39-
private VideoPlayerEventCallbacks(EventChannel.EventSink eventSink) {
41+
private VideoPlayerEventCallbacks(QueuingEventSink eventSink) {
4042
this.eventSink = eventSink;
4143
}
4244

4345
@Override
4446
public void onInitialized(
4547
int width, int height, long durationInMs, int rotationCorrectionInDegrees) {
46-
Map<String, Object> event = new HashMap<>();
47-
event.put("event", "initialized");
48-
event.put("width", width);
49-
event.put("height", height);
50-
event.put("duration", durationInMs);
51-
if (rotationCorrectionInDegrees != 0) {
52-
event.put("rotationCorrection", rotationCorrectionInDegrees);
53-
}
54-
eventSink.success(event);
48+
eventSink.success(
49+
new InitializationEvent(durationInMs, width, height, rotationCorrectionInDegrees));
5550
}
5651

5752
@Override
58-
public void onBufferingStart() {
59-
Map<String, Object> event = new HashMap<>();
60-
event.put("event", "bufferingStart");
61-
eventSink.success(event);
62-
}
63-
64-
@Override
65-
public void onBufferingUpdate(long bufferedPosition) {
66-
Map<String, Object> event = new HashMap<>();
67-
event.put("event", "bufferingUpdate");
68-
event.put("position", bufferedPosition);
69-
eventSink.success(event);
70-
}
71-
72-
@Override
73-
public void onBufferingEnd() {
74-
Map<String, Object> event = new HashMap<>();
75-
event.put("event", "bufferingEnd");
76-
eventSink.success(event);
77-
}
78-
79-
@Override
80-
public void onCompleted() {
81-
Map<String, Object> event = new HashMap<>();
82-
event.put("event", "completed");
83-
eventSink.success(event);
53+
public void onPlaybackStateChanged(@NonNull PlatformPlaybackState state) {
54+
eventSink.success(new PlaybackStateChangeEvent(state));
8455
}
8556

8657
@Override
@@ -90,9 +61,6 @@ public void onError(@NonNull String code, @Nullable String message, @Nullable Ob
9061

9162
@Override
9263
public void onIsPlayingStateUpdate(boolean isPlaying) {
93-
Map<String, Object> event = new HashMap<>();
94-
event.put("event", "isPlayingStateUpdate");
95-
event.put("isPlaying", isPlaying);
96-
eventSink.success(event);
64+
eventSink.success(new IsPlayingStateEvent(isPlaying));
9765
}
9866
}

0 commit comments

Comments
 (0)