Skip to content

Commit

Permalink
[video_player] Passing http headers to file constructor (#3266)
Browse files Browse the repository at this point in the history
[video_player] Passing http headers to file constructor
  • Loading branch information
abdelaziz-mahdy authored Mar 9, 2023
1 parent e10e945 commit 73e7ef7
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 35 deletions.
3 changes: 2 additions & 1 deletion packages/video_player/video_player/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 2.6.0

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

## 2.5.3
Expand Down
9 changes: 6 additions & 3 deletions packages/video_player/video_player/lib/video_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
/// null.
/// **Android only**: The [formatHint] option allows the caller to override
/// the video format detection code.
/// [httpHeaders] option allows to specify HTTP headers
/// [httpHeaders] option allows to specify HTTP headers.
/// for the request to the [dataSource].
VideoPlayerController.network(
this.dataSource, {
Expand All @@ -246,14 +246,16 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
/// Constructs a [VideoPlayerController] playing a video from a file.
///
/// This will load the file from a file:// URI constructed from [file]'s path.
/// [httpHeaders] option allows to specify HTTP headers, mainly used for hls files like (m3u8).
VideoPlayerController.file(File file,
{Future<ClosedCaptionFile>? closedCaptionFile, this.videoPlayerOptions})
{Future<ClosedCaptionFile>? closedCaptionFile,
this.videoPlayerOptions,
this.httpHeaders = const <String, String>{}})
: _closedCaptionFileFuture = closedCaptionFile,
dataSource = Uri.file(file.absolute.path).toString(),
dataSourceType = DataSourceType.file,
package = null,
formatHint = null,
httpHeaders = const <String, String>{},
super(VideoPlayerValue(duration: Duration.zero));

/// Constructs a [VideoPlayerController] playing a video from a contentUri.
Expand Down Expand Up @@ -344,6 +346,7 @@ class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {
dataSourceDescription = DataSource(
sourceType: DataSourceType.file,
uri: dataSource,
httpHeaders: httpHeaders,
);
break;
case DataSourceType.contentUri:
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Flutter plugin for displaying inline video with other Flutter
widgets on Android, iOS, and web.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.5.3
version: 2.6.0

environment:
sdk: ">=2.17.0 <3.0.0"
Expand Down
16 changes: 16 additions & 0 deletions packages/video_player/video_player/test/video_player_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,22 @@ void main() {
reason: 'Actual string: $uri');
}, skip: kIsWeb /* Web does not support file assets. */);

test('file with headers (m3u8)', () async {
final VideoPlayerController controller = VideoPlayerController.file(
File('a.avi'),
httpHeaders: <String, String>{'Authorization': 'Bearer token'},
);
await controller.initialize();

final String uri = fakeVideoPlayerPlatform.dataSources[0].uri!;
expect(uri.startsWith('file:///'), true, reason: 'Actual string: $uri');
expect(uri.endsWith('/a.avi'), true, reason: 'Actual string: $uri');

expect(
fakeVideoPlayerPlatform.dataSources[0].httpHeaders,
<String, String>{'Authorization': 'Bearer token'},
);
}, skip: kIsWeb /* Web does not support file assets. */);
test('successful initialize on controller with error clears error',
() async {
final VideoPlayerController controller = VideoPlayerController.network(
Expand Down
4 changes: 4 additions & 0 deletions packages/video_player/video_player_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.4.0

* Allows setting the ExoPlayer user agent by passing a User-Agent HTTP header.

## 2.3.12

* Clarifies explanation of endorsement in README.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,14 @@ final class VideoPlayer {

private final EventChannel eventChannel;

private static final String USER_AGENT = "User-Agent";

@VisibleForTesting boolean isInitialized = false;

private final VideoPlayerOptions options;

private DefaultHttpDataSource.Factory httpDataSourceFactory = new DefaultHttpDataSource.Factory();

VideoPlayer(
Context context,
EventChannel eventChannel,
Expand All @@ -73,23 +77,11 @@ final class VideoPlayer {
this.options = options;

ExoPlayer exoPlayer = new ExoPlayer.Builder(context).build();

Uri uri = Uri.parse(dataSource);
DataSource.Factory dataSourceFactory;

if (isHTTP(uri)) {
DefaultHttpDataSource.Factory httpDataSourceFactory =
new DefaultHttpDataSource.Factory()
.setUserAgent("ExoPlayer")
.setAllowCrossProtocolRedirects(true);

if (httpHeaders != null && !httpHeaders.isEmpty()) {
httpDataSourceFactory.setDefaultRequestProperties(httpHeaders);
}
dataSourceFactory = httpDataSourceFactory;
} else {
dataSourceFactory = new DefaultDataSource.Factory(context);
}
buildHttpDataSourceFactory(httpHeaders);
DataSource.Factory dataSourceFactory =
new DefaultDataSource.Factory(context, httpDataSourceFactory);

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

Expand All @@ -106,20 +98,29 @@ final class VideoPlayer {
EventChannel eventChannel,
TextureRegistry.SurfaceTextureEntry textureEntry,
VideoPlayerOptions options,
QueuingEventSink eventSink) {
QueuingEventSink eventSink,
DefaultHttpDataSource.Factory httpDataSourceFactory) {
this.eventChannel = eventChannel;
this.textureEntry = textureEntry;
this.options = options;
this.httpDataSourceFactory = httpDataSourceFactory;

setUpVideoPlayer(exoPlayer, eventSink);
}

private static boolean isHTTP(Uri uri) {
if (uri == null || uri.getScheme() == null) {
return false;
@VisibleForTesting
public void buildHttpDataSourceFactory(@NonNull Map<String, String> httpHeaders) {
final boolean httpHeadersNotEmpty = !httpHeaders.isEmpty();
final String userAgent =
httpHeadersNotEmpty && httpHeaders.containsKey(USER_AGENT)
? httpHeaders.get(USER_AGENT)
: "ExoPlayer";

httpDataSourceFactory.setUserAgent(userAgent).setAllowCrossProtocolRedirects(true);

if (httpHeadersNotEmpty) {
httpDataSourceFactory.setDefaultRequestProperties(httpHeaders);
}
String scheme = uri.getScheme();
return scheme.equals("http") || scheme.equals("https");
}

private MediaSource buildMediaSource(
Expand Down Expand Up @@ -149,13 +150,11 @@ private MediaSource buildMediaSource(
switch (type) {
case C.CONTENT_TYPE_SS:
return new SsMediaSource.Factory(
new DefaultSsChunkSource.Factory(mediaDataSourceFactory),
new DefaultDataSource.Factory(context, mediaDataSourceFactory))
new DefaultSsChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri));
case C.CONTENT_TYPE_DASH:
return new DashMediaSource.Factory(
new DefaultDashChunkSource.Factory(mediaDataSourceFactory),
new DefaultDataSource.Factory(context, mediaDataSourceFactory))
new DefaultDashChunkSource.Factory(mediaDataSourceFactory), mediaDataSourceFactory)
.createMediaSource(MediaItem.fromUri(uri));
case C.CONTENT_TYPE_HLS:
return new HlsMediaSource.Factory(mediaDataSourceFactory)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
package io.flutter.plugins.videoplayer;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import io.flutter.plugin.common.EventChannel;
import io.flutter.view.TextureRegistry;
import java.util.HashMap;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand All @@ -29,6 +34,7 @@ public class VideoPlayerTest {
private TextureRegistry.SurfaceTextureEntry fakeSurfaceTextureEntry;
private VideoPlayerOptions fakeVideoPlayerOptions;
private QueuingEventSink fakeEventSink;
private DefaultHttpDataSource.Factory httpDataSourceFactorySpy;

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

Expand All @@ -41,6 +47,76 @@ public void before() {
fakeSurfaceTextureEntry = mock(TextureRegistry.SurfaceTextureEntry.class);
fakeVideoPlayerOptions = mock(VideoPlayerOptions.class);
fakeEventSink = mock(QueuingEventSink.class);
httpDataSourceFactorySpy = spy(new DefaultHttpDataSource.Factory());
}

@Test
public void videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNull() {
VideoPlayer videoPlayer =
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);

videoPlayer.buildHttpDataSourceFactory(new HashMap<>());

verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
verify(httpDataSourceFactorySpy, never()).setDefaultRequestProperties(any());
}

@Test
public void
videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNonNullAndUserAgentSpecified() {
VideoPlayer videoPlayer =
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Map<String, String> httpHeaders =
new HashMap<String, String>() {
{
put("header", "value");
put("User-Agent", "userAgent");
}
};

videoPlayer.buildHttpDataSourceFactory(httpHeaders);

verify(httpDataSourceFactorySpy).setUserAgent("userAgent");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
verify(httpDataSourceFactorySpy).setDefaultRequestProperties(httpHeaders);
}

@Test
public void
videoPlayer_buildsHttpDataSourceFactoryProperlyWhenHttpHeadersNonNullAndUserAgentNotSpecified() {
VideoPlayer videoPlayer =
new VideoPlayer(
fakeExoPlayer,
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeVideoPlayerOptions,
fakeEventSink,
httpDataSourceFactorySpy);
Map<String, String> httpHeaders =
new HashMap<String, String>() {
{
put("header", "value");
}
};

videoPlayer.buildHttpDataSourceFactory(httpHeaders);

verify(httpDataSourceFactorySpy).setUserAgent("ExoPlayer");
verify(httpDataSourceFactorySpy).setAllowCrossProtocolRedirects(true);
verify(httpDataSourceFactorySpy).setDefaultRequestProperties(httpHeaders);
}

@Test
Expand All @@ -51,7 +127,8 @@ public void sendInitializedSendsExpectedEvent_90RotationDegrees() {
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeVideoPlayerOptions,
fakeEventSink);
fakeEventSink,
httpDataSourceFactorySpy);
Format testFormat =
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(90).build();

Expand Down Expand Up @@ -79,7 +156,8 @@ public void sendInitializedSendsExpectedEvent_270RotationDegrees() {
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeVideoPlayerOptions,
fakeEventSink);
fakeEventSink,
httpDataSourceFactorySpy);
Format testFormat =
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(270).build();

Expand Down Expand Up @@ -107,7 +185,8 @@ public void sendInitializedSendsExpectedEvent_0RotationDegrees() {
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeVideoPlayerOptions,
fakeEventSink);
fakeEventSink,
httpDataSourceFactorySpy);
Format testFormat =
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(0).build();

Expand Down Expand Up @@ -135,7 +214,8 @@ public void sendInitializedSendsExpectedEvent_180RotationDegrees() {
fakeEventChannel,
fakeSurfaceTextureEntry,
fakeVideoPlayerOptions,
fakeEventSink);
fakeEventSink,
httpDataSourceFactorySpy);
Format testFormat =
new Format.Builder().setWidth(100).setHeight(200).setRotationDegrees(180).build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class AndroidVideoPlayer extends VideoPlayerPlatform {
break;
case DataSourceType.file:
uri = dataSource.uri;
httpHeaders = dataSource.httpHeaders;
break;
case DataSourceType.contentUri:
uri = dataSource.uri;
Expand Down
2 changes: 1 addition & 1 deletion packages/video_player/video_player_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: video_player_android
description: Android implementation of the video_player plugin.
repository: https://github.com/flutter/packages/tree/main/packages/video_player/video_player_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+video_player%22
version: 2.3.12
version: 2.4.0

environment:
sdk: ">=2.17.0 <3.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,18 @@ void main() {
expect(textureId, 3);
});

test('create with file (some headers)', () async {
final int? textureId = await player.create(DataSource(
sourceType: DataSourceType.file,
uri: 'someUri',
httpHeaders: <String, String>{'Authorization': 'Bearer token'},
));
expect(log.log.last, 'create');
expect(log.createMessage?.uri, 'someUri');
expect(log.createMessage?.httpHeaders,
<String, String>{'Authorization': 'Bearer token'});
expect(textureId, 3);
});
test('setLooping', () async {
await player.setLooping(1, true);
expect(log.log.last, 'setLooping');
Expand Down

0 comments on commit 73e7ef7

Please sign in to comment.