[video_player] add http headers (#3671)

This enables to pass HTTP headers to VideoPlayerController.network.

Fixes: flutter/flutter#16466
diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md
index 08a5e44..6b20fd9 100644
--- a/packages/video_player/video_player/CHANGELOG.md
+++ b/packages/video_player/video_player/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.1.0
+
+* Add `httpHeaders` option to `VideoPlayerController.network`
+
 ## 2.0.2
 
 * Fix `VideoPlayerValue` size and aspect ratio documentation
diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java
index f1a9095..e0a4a3b 100644
--- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java
+++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// Autogenerated from Pigeon (v0.1.19), do not edit directly.
+// Autogenerated from Pigeon (v0.1.21), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 package io.flutter.plugins.videoplayer;
@@ -87,12 +87,23 @@
       this.formatHint = setterArg;
     }
 
+    private HashMap httpHeaders;
+
+    public HashMap getHttpHeaders() {
+      return httpHeaders;
+    }
+
+    public void setHttpHeaders(HashMap setterArg) {
+      this.httpHeaders = setterArg;
+    }
+
     HashMap toMap() {
       HashMap<String, Object> toMapResult = new HashMap<>();
       toMapResult.put("asset", asset);
       toMapResult.put("uri", uri);
       toMapResult.put("packageName", packageName);
       toMapResult.put("formatHint", formatHint);
+      toMapResult.put("httpHeaders", httpHeaders);
       return toMapResult;
     }
 
@@ -106,6 +117,8 @@
       fromMapResult.packageName = (String) packageName;
       Object formatHint = map.get("formatHint");
       fromMapResult.formatHint = (String) formatHint;
+      Object httpHeaders = map.get("httpHeaders");
+      fromMapResult.httpHeaders = (HashMap) httpHeaders;
       return fromMapResult;
     }
   }
diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
index 840b146..87784ee 100644
--- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
+++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java
@@ -65,6 +65,7 @@
       TextureRegistry.SurfaceTextureEntry textureEntry,
       String dataSource,
       String formatHint,
+      Map<String, String> httpHeaders,
       VideoPlayerOptions options) {
     this.eventChannel = eventChannel;
     this.textureEntry = textureEntry;
@@ -76,13 +77,17 @@
 
     DataSource.Factory dataSourceFactory;
     if (isHTTP(uri)) {
-      dataSourceFactory =
+      DefaultHttpDataSourceFactory httpDataSourceFactory =
           new DefaultHttpDataSourceFactory(
               "ExoPlayer",
               null,
               DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS,
               DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS,
               true);
+      if (httpHeaders != null && !httpHeaders.isEmpty()) {
+        httpDataSourceFactory.getDefaultRequestProperties().set(httpHeaders);
+      }
+      dataSourceFactory = httpDataSourceFactory;
     } else {
       dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer");
     }
diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
index 2895db2..d77b45e 100644
--- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
+++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java
@@ -23,6 +23,7 @@
 import io.flutter.view.TextureRegistry;
 import java.security.KeyManagementException;
 import java.security.NoSuchAlgorithmException;
+import java.util.Map;
 import javax.net.ssl.HttpsURLConnection;
 
 /** Android platform implementation of the VideoPlayerPlugin. */
@@ -138,8 +139,11 @@
               handle,
               "asset:///" + assetLookupKey,
               null,
+              null,
               options);
     } else {
+      @SuppressWarnings("unchecked")
+      Map<String, String> httpHeaders = arg.getHttpHeaders();
       player =
           new VideoPlayer(
               flutterState.applicationContext,
@@ -147,6 +151,7 @@
               handle,
               arg.getUri(),
               arg.getFormatHint(),
+              httpHeaders,
               options);
     }
     videoPlayers.put(handle.id(), player);
diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m
index 83144a9..b359c1b 100644
--- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m
+++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m
@@ -46,7 +46,9 @@
 @property(nonatomic, readonly) bool isPlaying;
 @property(nonatomic) bool isLooping;
 @property(nonatomic, readonly) bool isInitialized;
-- (instancetype)initWithURL:(NSURL*)url frameUpdater:(FLTFrameUpdater*)frameUpdater;
+- (instancetype)initWithURL:(NSURL*)url
+               frameUpdater:(FLTFrameUpdater*)frameUpdater
+                httpHeaders:(NSDictionary<NSString*, NSString*>*)headers;
 - (void)play;
 - (void)pause;
 - (void)setIsLooping:(bool)isLooping;
@@ -62,7 +64,7 @@
 @implementation FLTVideoPlayer
 - (instancetype)initWithAsset:(NSString*)asset frameUpdater:(FLTFrameUpdater*)frameUpdater {
   NSString* path = [[NSBundle mainBundle] pathForResource:asset ofType:nil];
-  return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater];
+  return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:nil];
 }
 
 - (void)addObservers:(AVPlayerItem*)item {
@@ -162,8 +164,15 @@
   _displayLink.paused = YES;
 }
 
-- (instancetype)initWithURL:(NSURL*)url frameUpdater:(FLTFrameUpdater*)frameUpdater {
-  AVPlayerItem* item = [AVPlayerItem playerItemWithURL:url];
+- (instancetype)initWithURL:(NSURL*)url
+               frameUpdater:(FLTFrameUpdater*)frameUpdater
+                httpHeaders:(NSDictionary<NSString*, NSString*>*)headers {
+  NSDictionary<NSString*, id>* options = nil;
+  if (headers != nil && [headers count] != 0) {
+    options = @{@"AVURLAssetHTTPHeaderFieldsKey" : headers};
+  }
+  AVURLAsset* urlAsset = [AVURLAsset URLAssetWithURL:url options:options];
+  AVPlayerItem* item = [AVPlayerItem playerItemWithAsset:urlAsset];
   return [self initWithPlayerItem:item frameUpdater:frameUpdater];
 }
 
@@ -522,7 +531,8 @@
     return [self onPlayerSetup:player frameUpdater:frameUpdater];
   } else if (input.uri) {
     player = [[FLTVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri]
-                                    frameUpdater:frameUpdater];
+                                    frameUpdater:frameUpdater
+                                     httpHeaders:input.httpHeaders];
     return [self onPlayerSetup:player frameUpdater:frameUpdater];
   } else {
     *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil];
diff --git a/packages/video_player/video_player/ios/Classes/messages.h b/packages/video_player/video_player/ios/Classes/messages.h
index 9717f65..e21e786 100644
--- a/packages/video_player/video_player/ios/Classes/messages.h
+++ b/packages/video_player/video_player/ios/Classes/messages.h
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// Autogenerated from Pigeon (v0.1.19), do not edit directly.
+// Autogenerated from Pigeon (v0.1.21), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 #import <Foundation/Foundation.h>
 @protocol FlutterBinaryMessenger;
@@ -28,6 +28,7 @@
 @property(nonatomic, copy, nullable) NSString *uri;
 @property(nonatomic, copy, nullable) NSString *packageName;
 @property(nonatomic, copy, nullable) NSString *formatHint;
+@property(nonatomic, strong, nullable) NSDictionary *httpHeaders;
 @end
 
 @interface FLTLoopingMessage : NSObject
diff --git a/packages/video_player/video_player/ios/Classes/messages.m b/packages/video_player/video_player/ios/Classes/messages.m
index 0993c94..14e375b 100644
--- a/packages/video_player/video_player/ios/Classes/messages.m
+++ b/packages/video_player/video_player/ios/Classes/messages.m
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-// Autogenerated from Pigeon (v0.1.19), do not edit directly.
+// Autogenerated from Pigeon (v0.1.21), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 #import "messages.h"
 #import <Flutter/Flutter.h>
@@ -11,18 +11,19 @@
 #error File requires ARC to be enabled.
 #endif
 
-#ifndef __clang_analyzer__
-static NSDictionary *wrapResult(NSDictionary *result, FlutterError *error) {
+static NSDictionary<NSString *, id> *wrapResult(NSDictionary *result, FlutterError *error) {
   NSDictionary *errorDict = (NSDictionary *)[NSNull null];
   if (error) {
-    errorDict = [NSDictionary
-        dictionaryWithObjectsAndKeys:(error.code ? error.code : [NSNull null]), @"code",
-                                     (error.message ? error.message : [NSNull null]), @"message",
-                                     (error.details ? error.details : [NSNull null]), @"details",
-                                     nil];
+    errorDict = @{
+      @"code" : (error.code ? error.code : [NSNull null]),
+      @"message" : (error.message ? error.message : [NSNull null]),
+      @"details" : (error.details ? error.details : [NSNull null]),
+    };
   }
-  return [NSDictionary dictionaryWithObjectsAndKeys:(result ? result : [NSNull null]), @"result",
-                                                    errorDict, @"error", nil];
+  return @{
+    @"result" : (result ? result : [NSNull null]),
+    @"error" : errorDict,
+  };
 }
 
 @interface FLTTextureMessage ()
@@ -89,6 +90,10 @@
   if ((NSNull *)result.formatHint == [NSNull null]) {
     result.formatHint = nil;
   }
+  result.httpHeaders = dict[@"httpHeaders"];
+  if ((NSNull *)result.httpHeaders == [NSNull null]) {
+    result.httpHeaders = nil;
+  }
   return result;
 }
 - (NSDictionary *)toMap {
@@ -98,7 +103,9 @@
                                    (self.packageName ? self.packageName : [NSNull null]),
                                    @"packageName",
                                    (self.formatHint ? self.formatHint : [NSNull null]),
-                                   @"formatHint", nil];
+                                   @"formatHint",
+                                   (self.httpHeaders ? self.httpHeaders : [NSNull null]),
+                                   @"httpHeaders", nil];
 }
 @end
 
@@ -221,8 +228,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTCreateMessage *input = [FLTCreateMessage fromMap:message];
+        FlutterError *error;
         FLTTextureMessage *output = [api create:input error:&error];
         callback(wrapResult([output toMap], error));
       }];
@@ -236,8 +243,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTTextureMessage *input = [FLTTextureMessage fromMap:message];
+        FlutterError *error;
         [api dispose:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -251,8 +258,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTLoopingMessage *input = [FLTLoopingMessage fromMap:message];
+        FlutterError *error;
         [api setLooping:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -266,8 +273,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTVolumeMessage *input = [FLTVolumeMessage fromMap:message];
+        FlutterError *error;
         [api setVolume:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -281,8 +288,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTPlaybackSpeedMessage *input = [FLTPlaybackSpeedMessage fromMap:message];
+        FlutterError *error;
         [api setPlaybackSpeed:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -296,8 +303,8 @@
                                            binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTTextureMessage *input = [FLTTextureMessage fromMap:message];
+        FlutterError *error;
         [api play:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -311,8 +318,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTTextureMessage *input = [FLTTextureMessage fromMap:message];
+        FlutterError *error;
         FLTPositionMessage *output = [api position:input error:&error];
         callback(wrapResult([output toMap], error));
       }];
@@ -326,8 +333,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTPositionMessage *input = [FLTPositionMessage fromMap:message];
+        FlutterError *error;
         [api seekTo:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -341,8 +348,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTTextureMessage *input = [FLTTextureMessage fromMap:message];
+        FlutterError *error;
         [api pause:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -356,8 +363,8 @@
                binaryMessenger:binaryMessenger];
     if (api) {
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
-        FlutterError *error;
         FLTMixWithOthersMessage *input = [FLTMixWithOthersMessage fromMap:message];
+        FlutterError *error;
         [api setMixWithOthers:input error:&error];
         callback(wrapResult(nil, error));
       }];
@@ -366,4 +373,3 @@
     }
   }
 }
-#endif
diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart
index 08bd1d4..d5bd7d2 100644
--- a/packages/video_player/video_player/lib/video_player.dart
+++ b/packages/video_player/video_player/lib/video_player.dart
@@ -187,6 +187,7 @@
       {this.package, this.closedCaptionFile, this.videoPlayerOptions})
       : dataSourceType = DataSourceType.asset,
         formatHint = null,
+        httpHeaders = const {},
         super(VideoPlayerValue(duration: Duration.zero));
 
   /// Constructs a [VideoPlayerController] playing a video from obtained from
@@ -196,9 +197,15 @@
   /// null.
   /// **Android only**: The [formatHint] option allows the caller to override
   /// the video format detection code.
-  VideoPlayerController.network(this.dataSource,
-      {this.formatHint, this.closedCaptionFile, this.videoPlayerOptions})
-      : dataSourceType = DataSourceType.network,
+  /// [httpHeaders] option allows to specify HTTP headers
+  /// for the request to the [dataSource].
+  VideoPlayerController.network(
+    this.dataSource, {
+    this.formatHint,
+    this.closedCaptionFile,
+    this.videoPlayerOptions,
+    this.httpHeaders = const {},
+  })  : dataSourceType = DataSourceType.network,
         package = null,
         super(VideoPlayerValue(duration: Duration.zero));
 
@@ -212,12 +219,18 @@
         dataSourceType = DataSourceType.file,
         package = null,
         formatHint = null,
+        httpHeaders = const {},
         super(VideoPlayerValue(duration: Duration.zero));
 
   /// The URI to the video file. This will be in different formats depending on
   /// the [DataSourceType] of the original video.
   final String dataSource;
 
+  /// HTTP headers used for the request to the [dataSource].
+  /// Only for [VideoPlayerController.network].
+  /// Always empty for other video types.
+  final Map<String, String> httpHeaders;
+
   /// **Android only**. Will override the platform's generic file format
   /// detection with whatever is set here.
   final VideoFormat? formatHint;
@@ -276,6 +289,7 @@
           sourceType: DataSourceType.network,
           uri: dataSource,
           formatHint: formatHint,
+          httpHeaders: httpHeaders,
         );
         break;
       case DataSourceType.file:
diff --git a/packages/video_player/video_player/pigeons/messages.dart b/packages/video_player/video_player/pigeons/messages.dart
index c0a76dd..e893aaa 100644
--- a/packages/video_player/video_player/pigeons/messages.dart
+++ b/packages/video_player/video_player/pigeons/messages.dart
@@ -35,6 +35,7 @@
   String uri;
   String packageName;
   String formatHint;
+  Map<String, String> httpHeaders;
 }
 
 class MixWithOthersMessage {
diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml
index 17442d7..0215ead 100644
--- a/packages/video_player/video_player/pubspec.yaml
+++ b/packages/video_player/video_player/pubspec.yaml
@@ -1,7 +1,7 @@
 name: video_player
 description: Flutter plugin for displaying inline video with other Flutter
   widgets on Android, iOS, and web.
-version: 2.0.2
+version: 2.1.0
 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player
 
 flutter:
@@ -17,7 +17,7 @@
 
 dependencies:
   meta: ^1.3.0
-  video_player_platform_interface: ^4.0.0
+  video_player_platform_interface: ^4.1.0
 
   # The design on https://flutter.dev/go/federated-plugins was to leave
   # this constraint as "any". We cannot do it right now as it fails pub publish
diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart
index 580c9ad..e17dac7 100644
--- a/packages/video_player/video_player/test/video_player_test.dart
+++ b/packages/video_player/video_player/test/video_player_test.dart
@@ -31,6 +31,9 @@
   String get dataSource => '';
 
   @override
+  Map<String, String> get httpHeaders => {};
+
+  @override
   DataSourceType get dataSourceType => DataSourceType.file;
 
   @override
@@ -200,22 +203,60 @@
         );
         await controller.initialize();
 
-        expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
-            'https://127.0.0.1');
         expect(
-            fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, null);
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
+          'https://127.0.0.1',
+        );
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint,
+          null,
+        );
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders,
+          {},
+        );
       });
 
       test('network with hint', () async {
         final VideoPlayerController controller = VideoPlayerController.network(
-            'https://127.0.0.1',
-            formatHint: VideoFormat.dash);
+          'https://127.0.0.1',
+          formatHint: VideoFormat.dash,
+        );
         await controller.initialize();
 
-        expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
-            'https://127.0.0.1');
-        expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint,
-            'dash');
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
+          'https://127.0.0.1',
+        );
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint,
+          'dash',
+        );
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders,
+          {},
+        );
+      });
+
+      test('network with some headers', () async {
+        final VideoPlayerController controller = VideoPlayerController.network(
+          'https://127.0.0.1',
+          httpHeaders: {'Authorization': 'Bearer token'},
+        );
+        await controller.initialize();
+
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri,
+          'https://127.0.0.1',
+        );
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint,
+          null,
+        );
+        expect(
+          fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders,
+          {'Authorization': 'Bearer token'},
+        );
       });
 
       test('init errors', () async {