Skip to content

Commit 89bf3e4

Browse files
Revert "Revert "[video_player] Passing http headers to file constructor (flutter#3266)" (flutter#3424)"
This reverts commit 88ca9e2.
1 parent 8a7dead commit 89bf3e4

File tree

10 files changed

+151
-35
lines changed

10 files changed

+151
-35
lines changed

packages/video_player/video_player/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
## NEXT
1+
## 2.6.0
22

3+
* Adds option to configure HTTP headers via `VideoPlayerController` to fix access to M3U8 files on Android.
34
* Aligns Dart and Flutter SDK constraints.
45

56
## 2.5.3

packages/video_player/video_player/lib/video_player.dart

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
230230
/// null.
231231
/// **Android only**: The [formatHint] option allows the caller to override
232232
/// the video format detection code.
233-
/// [httpHeaders] option allows to specify HTTP headers
233+
/// [httpHeaders] option allows to specify HTTP headers.
234234
/// for the request to the [dataSource].
235235
VideoPlayerController.network(
236236
this.dataSource, {
@@ -246,14 +246,16 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
246246
/// Constructs a [VideoPlayerController] playing a video from a file.
247247
///
248248
/// This will load the file from a file:// URI constructed from [file]'s path.
249+
/// [httpHeaders] option allows to specify HTTP headers, mainly used for hls files like (m3u8).
249250
VideoPlayerController.file(File file,
250-
{Future<ClosedCaptionFile>? closedCaptionFile, this.videoPlayerOptions})
251+
{Future<ClosedCaptionFile>? closedCaptionFile,
252+
this.videoPlayerOptions,
253+
this.httpHeaders = const <String, String>{}})
251254
: _closedCaptionFileFuture = closedCaptionFile,
252255
dataSource = Uri.file(file.absolute.path).toString(),
253256
dataSourceType = DataSourceType.file,
254257
package = null,
255258
formatHint = null,
256-
httpHeaders = const <String, String>{},
257259
super(VideoPlayerValue(duration: Duration.zero));
258260

259261
/// Constructs a [VideoPlayerController] playing a video from a contentUri.
@@ -344,6 +346,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
344346
dataSourceDescription = DataSource(
345347
sourceType: DataSourceType.file,
346348
uri: dataSource,
349+
httpHeaders: httpHeaders,
347350
);
348351
break;
349352
case DataSourceType.contentUri:

packages/video_player/video_player/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
33
widgets on Android, iOS, and web.
44
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
6-
version: 2.5.3
6+
version: 2.6.0
77

88
environment:
99
sdk: ">=2.17.0 <3.0.0"

packages/video_player/video_player/test/video_player_test.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,22 @@ void main() {
358358
reason: 'Actual string: $uri');
359359
}, skip: kIsWeb /* Web does not support file assets. */);
360360

361+
test('file with headers (m3u8)', () async {
362+
final VideoPlayerController controller = VideoPlayerController.file(
363+
File('a.avi'),
364+
httpHeaders: <String, String>{'Authorization': 'Bearer token'},
365+
);
366+
await controller.initialize();
367+
368+
final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!;
369+
expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri');
370+
expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri');
371+
372+
expect(
373+
fakeVideoPlayerPlatform.dataSources[0].httpHeaders,
374+
<String, String>{'Authorization': 'Bearer token'},
375+
);
376+
}, skip: kIsWeb /* Web does not support file assets. */);
361377
test('successful initialize on controller with error clears error',
362378
() async {
363379
final VideoPlayerController controller = VideoPlayerController.network(

packages/video_player/video_player_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.4.0
2+
3+
* Allows setting the ExoPlayer user agent by passing a User-Agent HTTP header.
4+
15
## 2.3.12
26

37
* Clarifies explanation of endorsement in README.

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

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,14 @@ final class VideoPlayer {
5656

5757
private final EventChannel eventChannel;
5858

59+
private static final String USER_AGENT = "User-Agent";
60+
5961
@VisibleForTesting boolean isInitialized = false;
6062

6163
private final VideoPlayerOptions options;
6264

65+
private DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory();
66+
6367
VideoPlayer(
6468
Context context,
6569
EventChannel eventChannel,
@@ -73,23 +77,11 @@ final class VideoPlayer {
7377
this.options = options;
7478

7579
ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();
76-
7780
Uri uri = Uri.parse(dataSource);
78-
DataSource.Factory dataSourceFactory;
7981

80-
if (isHTTP(uri)) {
81-
DefaultHttpDataSource.Factory httpDataSourceFactory =
82-
new DefaultHttpDataSource.Factory()
83-
.setUserAgent("ExoPlayer")
84-
.setAllowCrossProtocolRedirects(true);
85-
86-
if (httpHeaders != null && !httpHeaders.isEmpty()) {
87-
httpDataSourceFactory.setDefaultRequestProperties(httpHeaders);
88-
}
89-
dataSourceFactory = httpDataSourceFactory;
90-
} else {
91-
dataSourceFactory = new DefaultDataSource.Factory(context);
92-
}
82+
buildHttpDataSourceFactory(httpHeaders);
83+
DataSource.Factory dataSourceFactory =
84+
new DefaultDataSource.Factory(context, httpDataSourceFactory);
9385

9486
MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context);
9587

@@ -106,20 +98,29 @@ final class VideoPlayer {
10698
EventChannel eventChannel,
10799
TextureRegistry.SurfaceTextureEntry textureEntry,
108100
VideoPlayerOptions options,
109-
QueuingEventSink eventSink) {
101+
QueuingEventSink eventSink,
102+
DefaultHttpDataSource.Factory httpDataSourceFactory) {
110103
this.eventChannel = eventChannel;
111104
this.textureEntry = textureEntry;
112105
this.options = options;
106+
this.httpDataSourceFactory = httpDataSourceFactory;
113107

114108
setUpVideoPlayer(exoPlayer, eventSink);
115109
}
116110

117-
private static boolean isHTTP(Uri uri) {
118-
if (uri == null || uri.getScheme() == null) {
119-
return false;
111+
@VisibleForTesting
112+
public void buildHttpDataSourceFactory(@NonNull Map<String, String> httpHeaders) {
113+
final boolean httpHeadersNotEmpty = !httpHeaders.isEmpty();
114+
final String userAgent =
115+
httpHeadersNotEmpty && httpHeaders.containsKey(USER_AGENT)
116+
? httpHeaders.get(USER_AGENT)
117+
: "ExoPlayer";
118+
119+
httpDataSourceFactory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);
120+
121+
if (httpHeadersNotEmpty) {
122+
httpDataSourceFactory.setDefaultRequestProperties(httpHeaders);
120123
}
121-
String scheme = uri.getScheme();
122-
return scheme.equals("http") || scheme.equals("https");
123124
}
124125

125126
private MediaSource buildMediaSource(
@@ -149,13 +150,11 @@ private MediaSource buildMediaSource(
149150
switch (type) {
150151
case C.CONTENT_TYPE_SS:
151152
return new SsMediaSource.Factory(
152-
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
153-
new DefaultDataSource.Factory(context, mediaDataSourceFactory))
153+
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
154154
.createMediaSource(MediaItem.fromUri(uri));
155155
case C.CONTENT_TYPE_DASH:
156156
return new DashMediaSource.Factory(
157-
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
158-
new DefaultDataSource.Factory(context, mediaDataSourceFactory))
157+
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
159158
.createMediaSource(MediaItem.fromUri(uri));
160159
case C.CONTENT_TYPE_HLS:
161160
return new HlsMediaSource.Factory(mediaDataSourceFactory)

packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
package io.flutter.plugins.videoplayer;
66

77
import static org.junit.Assert.assertEquals;
8+
import static org.mockito.Mockito.any;
89
import static org.mockito.Mockito.mock;
10+
import static org.mockito.Mockito.never;
11+
import static org.mockito.Mockito.spy;
912
import static org.mockito.Mockito.verify;
1013
import static org.mockito.Mockito.when;
1114

1215
import com.google.android.exoplayer2.ExoPlayer;
1316
import com.google.android.exoplayer2.Format;
17+
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
1418
import io.flutter.plugin.common.EventChannel;
1519
import io.flutter.view.TextureRegistry;
1620
import java.util.HashMap;
21+
import java.util.Map;
1722
import org.junit.Before;
1823
import org.junit.Test;
1924
import org.junit.runner.RunWith;
@@ -29,6 +34,7 @@ public class VideoPlayerTest {
2934
private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry;
3035
private VideoPlayerOptions fakeVideoPlayerOptions;
3136
private QueuingEventSink fakeEventSink;
37+
private DefaultHttpDataSource.Factory httpDataSourceFactorySpy;
3238

3339
@Captor private ArgumentCaptor<HashMap<String, Object>> eventCaptor;
3440

@@ -41,6 +47,76 @@ public void before() {
4147
fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class);
4248
fakeVideoPlayerOptions = mock(VideoPlayerOptions.class);
4349
fakeEventSink = mock(QueuingEventSink.class);
50+
httpDataSourceFactorySpy = spy(new DefaultHttpDataSource.Factory());
51+
}
52+
53+
@Test
54+
public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() {
55+
VideoPlayer videoPlayer =
56+
new VideoPlayer(
57+
fakeExoPlayer,
58+
fakeEventChannel,
59+
fakeSurfaceTextureEntry,
60+
fakeVideoPlayerOptions,
61+
fakeEventSink,
62+
httpDataSourceFactorySpy);
63+
64+
videoPlayer.buildHttpDataSourceFactory(new HashMap<>());
65+
66+
verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
67+
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
68+
verify(httpDataSourceFactorySpy, never()).setDefaultRequestProperties(any());
69+
}
70+
71+
@Test
72+
public void
73+
videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNonNullAndUserAgentSpecified() {
74+
VideoPlayer videoPlayer =
75+
new VideoPlayer(
76+
fakeExoPlayer,
77+
fakeEventChannel,
78+
fakeSurfaceTextureEntry,
79+
fakeVideoPlayerOptions,
80+
fakeEventSink,
81+
httpDataSourceFactorySpy);
82+
Map<String, String> httpHeaders =
83+
new HashMap<String, String>() {
84+
{
85+
put("header", "value");
86+
put("User-Agent", "userAgent");
87+
}
88+
};
89+
90+
videoPlayer.buildHttpDataSourceFactory(httpHeaders);
91+
92+
verify(httpDataSourceFactorySpy).setUserAgent("userAgent");
93+
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
94+
verify(httpDataSourceFactorySpy).setDefaultRequestProperties(httpHeaders);
95+
}
96+
97+
@Test
98+
public void
99+
videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNonNullAndUserAgentNotSpecified() {
100+
VideoPlayer videoPlayer =
101+
new VideoPlayer(
102+
fakeExoPlayer,
103+
fakeEventChannel,
104+
fakeSurfaceTextureEntry,
105+
fakeVideoPlayerOptions,
106+
fakeEventSink,
107+
httpDataSourceFactorySpy);
108+
Map<String, String> httpHeaders =
109+
new HashMap<String, String>() {
110+
{
111+
put("header", "value");
112+
}
113+
};
114+
115+
videoPlayer.buildHttpDataSourceFactory(httpHeaders);
116+
117+
verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
118+
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
119+
verify(httpDataSourceFactorySpy).setDefaultRequestProperties(httpHeaders);
44120
}
45121

46122
@Test
@@ -51,7 +127,8 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() {
51127
fakeEventChannel,
52128
fakeSurfaceTextureEntry,
53129
fakeVideoPlayerOptions,
54-
fakeEventSink);
130+
fakeEventSink,
131+
httpDataSourceFactorySpy);
55132
Format testFormat =
56133
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build();
57134

@@ -79,7 +156,8 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() {
79156
fakeEventChannel,
80157
fakeSurfaceTextureEntry,
81158
fakeVideoPlayerOptions,
82-
fakeEventSink);
159+
fakeEventSink,
160+
httpDataSourceFactorySpy);
83161
Format testFormat =
84162
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build();
85163

@@ -107,7 +185,8 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() {
107185
fakeEventChannel,
108186
fakeSurfaceTextureEntry,
109187
fakeVideoPlayerOptions,
110-
fakeEventSink);
188+
fakeEventSink,
189+
httpDataSourceFactorySpy);
111190
Format testFormat =
112191
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build();
113192

@@ -135,7 +214,8 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() {
135214
fakeEventChannel,
136215
fakeSurfaceTextureEntry,
137216
fakeVideoPlayerOptions,
138-
fakeEventSink);
217+
fakeEventSink,
218+
httpDataSourceFactorySpy);
139219
Format testFormat =
140220
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build();
141221

packages/video_player/video_player_android/lib/src/android_video_player.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {
4949
break;
5050
case DataSourceType.file:
5151
uri = dataSource.uri;
52+
httpHeaders = dataSource.httpHeaders;
5253
break;
5354
case DataSourceType.contentUri:
5455
uri = dataSource.uri;

packages/video_player/video_player_android/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: video_player_android
22
description: Android implementation of the video_player plugin.
33
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
44
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
5-
version: 2.3.12
5+
version: 2.4.0
66

77
environment:
88
sdk: ">=2.17.0 <3.0.0"

packages/video_player/video_player_android/test/android_video_player_test.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,18 @@ void main() {
176176
expect(textureId, 3);
177177
});
178178

179+
test('create with file (some headers)', () async {
180+
final int? textureId = await player.create(DataSource(
181+
sourceType: DataSourceType.file,
182+
uri: 'someUri',
183+
httpHeaders: <String, String>{'Authorization': 'Bearer token'},
184+
));
185+
expect(log.log.last, 'create');
186+
expect(log.createMessage?.uri, 'someUri');
187+
expect(log.createMessage?.httpHeaders,
188+
<String, String>{'Authorization': 'Bearer token'});
189+
expect(textureId, 3);
190+
});
179191
test('setLooping', () async {
180192
await player.setLooping(1, true);
181193
expect(log.log.last, 'setLooping');

0 commit comments

Comments
 (0)