diff --git a/packages/video_player/video_player_android/AUTHORS b/packages/video_player/video_player_android/AUTHORS index fc16c35c4c250..07a1e9f7a118e 100644 --- a/packages/video_player/video_player_android/AUTHORS +++ b/packages/video_player/video_player_android/AUTHORS @@ -65,3 +65,4 @@ Anton Borries Alex Li Rahul Raj <64.rahulraj@gmail.com> Márton Matuz +André Sousa diff --git a/packages/video_player/video_player_android/CHANGELOG.md b/packages/video_player/video_player_android/CHANGELOG.md index 911555aa59283..e6815fce6ef18 100644 --- a/packages/video_player/video_player_android/CHANGELOG.md +++ b/packages/video_player/video_player_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.6.0 + +* Adds RTSP support. + ## 2.5.4 * Updates Media3-ExoPlayer to 1.4.0. diff --git a/packages/video_player/video_player_android/android/build.gradle b/packages/video_player/video_player_android/android/build.gradle index d9ec79e5ab74e..af5008314a718 100644 --- a/packages/video_player/video_player_android/android/build.gradle +++ b/packages/video_player/video_player_android/android/build.gradle @@ -52,6 +52,7 @@ android { implementation "androidx.media3:media3-exoplayer:${exoplayer_version}" implementation "androidx.media3:media3-exoplayer-hls:${exoplayer_version}" implementation "androidx.media3:media3-exoplayer-dash:${exoplayer_version}" + implementation "androidx.media3:media3-exoplayer-rtsp:${exoplayer_version}" implementation "androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}" testImplementation 'junit:junit:4.13.2' testImplementation 'androidx.test:core:1.3.0' diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RemoteVideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java similarity index 95% rename from packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RemoteVideoAsset.java rename to packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java index 75c3c42d96f9e..f29efc3cab673 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RemoteVideoAsset.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/HttpVideoAsset.java @@ -19,14 +19,14 @@ import androidx.media3.exoplayer.source.MediaSource; import java.util.Map; -final class RemoteVideoAsset extends VideoAsset { +final class HttpVideoAsset extends VideoAsset { private static final String DEFAULT_USER_AGENT = "ExoPlayer"; private static final String HEADER_USER_AGENT = "User-Agent"; @NonNull private final StreamingFormat streamingFormat; @NonNull private final Map httpHeaders; - RemoteVideoAsset( + HttpVideoAsset( @Nullable String assetUrl, @NonNull StreamingFormat streamingFormat, @NonNull Map httpHeaders) { @@ -79,8 +79,8 @@ MediaSource.Factory getMediaSourceFactory( userAgent = httpHeaders.get(HEADER_USER_AGENT); } unstableUpdateDataSourceFactory(initialFactory, httpHeaders, userAgent); - DataSource.Factory dataSoruceFactory = new DefaultDataSource.Factory(context, initialFactory); - return new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSoruceFactory); + DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, initialFactory); + return new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory); } // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java new file mode 100644 index 0000000000000..1eb87c8bac0f8 --- /dev/null +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/RtspVideoAsset.java @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.media3.common.MediaItem; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.rtsp.RtspMediaSource; +import androidx.media3.exoplayer.source.MediaSource; + +final class RtspVideoAsset extends VideoAsset { + RtspVideoAsset(@NonNull String assetUrl) { + super(assetUrl); + } + + @NonNull + @Override + MediaItem getMediaItem() { + return new MediaItem.Builder().setUri(assetUrl).build(); + } + + // TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039. + @OptIn(markerClass = UnstableApi.class) + @Override + MediaSource.Factory getMediaSourceFactory(Context context) { + return new RtspMediaSource.Factory(); + } +} diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java index 2b83437c6fc65..3fab758e52f7d 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoAsset.java @@ -41,7 +41,21 @@ static VideoAsset fromRemoteUrl( @Nullable String remoteUrl, @NonNull StreamingFormat streamingFormat, @NonNull Map httpHeaders) { - return new RemoteVideoAsset(remoteUrl, streamingFormat, new HashMap<>(httpHeaders)); + return new HttpVideoAsset(remoteUrl, streamingFormat, new HashMap<>(httpHeaders)); + } + + /** + * Returns an asset from a RTSP URL. + * + * @param rtspUrl remote asset, beginning with {@code rtsp://}. + * @return the asset. + */ + @NonNull + static VideoAsset fromRtspUrl(@NonNull String rtspUrl) { + if (!rtspUrl.startsWith("rtsp://")) { + throw new IllegalArgumentException("rtspUrl must start with 'rtsp://'"); + } + return new RtspVideoAsset(rtspUrl); } @Nullable protected final String assetUrl; diff --git a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 0e57068944e1b..af13c53955122 100644 --- a/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player_android/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -110,6 +110,8 @@ public void initialize() { assetLookupKey = flutterState.keyForAsset.get(arg.getAsset()); } videoAsset = VideoAsset.fromAssetUrl("asset:///" + assetLookupKey); + } else if (arg.getUri().startsWith("rtsp://")) { + videoAsset = VideoAsset.fromRtspUrl(arg.getUri()); } else { Map httpHeaders = arg.getHttpHeaders(); VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN; diff --git a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoAssetTest.java b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoAssetTest.java index 743bf572aee23..6e95d0504c3d0 100644 --- a/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoAssetTest.java +++ b/packages/video_player/video_player_android/android/src/test/java/io/flutter/plugins/videoplayer/VideoAssetTest.java @@ -69,8 +69,8 @@ public void remoteVideoByDefaultSetsUserAgentAndCrossProtocolRedirects() { DefaultHttpDataSource.Factory mockFactory = mockHttpFactory(); - // Cast to RemoteVideoAsset to call a testing-only method to intercept calls. - ((RemoteVideoAsset) asset) + // Cast to HttpVideoAsset to call a testing-only method to intercept calls. + ((HttpVideoAsset) asset) .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory); verify(mockFactory).setUserAgent("ExoPlayer"); @@ -89,8 +89,8 @@ public void remoteVideoOverridesUserAgentIfProvided() { DefaultHttpDataSource.Factory mockFactory = mockHttpFactory(); - // Cast to RemoteVideoAsset to call a testing-only method to intercept calls. - ((RemoteVideoAsset) asset) + // Cast to HttpVideoAsset to call a testing-only method to intercept calls. + ((HttpVideoAsset) asset) .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory); verify(mockFactory).setUserAgent("FantasticalVideoBot"); @@ -127,12 +127,28 @@ public void remoteVideoSetsAdditionalHttpHeadersIfProvided() { DefaultHttpDataSource.Factory mockFactory = mockHttpFactory(); - // Cast to RemoteVideoAsset to call a testing-only method to intercept calls. - ((RemoteVideoAsset) asset) + // Cast to HttpVideoAsset to call a testing-only method to intercept calls. + ((HttpVideoAsset) asset) .getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory); verify(mockFactory).setUserAgent("ExoPlayer"); verify(mockFactory).setAllowCrossProtocolRedirects(true); verify(mockFactory).setDefaultRequestProperties(headers); } + + @Test + public void rtspVideoRequiresRtspUrl() { + assertThrows( + IllegalArgumentException.class, () -> VideoAsset.fromRtspUrl("https://not.rtsp/video.mp4")); + } + + @Test + public void rtspVideoCreatesMediaItem() { + VideoAsset asset = VideoAsset.fromRtspUrl("rtsp://test:pass@flutter.dev/stream"); + MediaItem mediaItem = asset.getMediaItem(); + + assert mediaItem.localConfiguration != null; + assertEquals( + mediaItem.localConfiguration.uri, Uri.parse("rtsp://test:pass@flutter.dev/stream")); + } } diff --git a/packages/video_player/video_player_android/example/lib/main.dart b/packages/video_player/video_player_android/example/lib/main.dart index df28b39ac76ba..79f4963bbfa7b 100644 --- a/packages/video_player/video_player_android/example/lib/main.dart +++ b/packages/video_player/video_player_android/example/lib/main.dart @@ -20,7 +20,7 @@ class _App extends StatelessWidget { @override Widget build(BuildContext context) { return DefaultTabController( - length: 2, + length: 3, child: Scaffold( key: const ValueKey('home_page'), appBar: AppBar( @@ -28,10 +28,8 @@ class _App extends StatelessWidget { bottom: const TabBar( isScrollable: true, tabs: [ - Tab( - icon: Icon(Icons.cloud), - text: 'Remote', - ), + Tab(icon: Icon(Icons.cloud), text: 'Remote'), + Tab(icon: Icon(Icons.videocam), text: 'RTSP'), Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'), ], ), @@ -39,6 +37,7 @@ class _App extends StatelessWidget { body: TabBarView( children: [ _BumbleBeeRemoteVideo(), + _RtspRemoteVideo(), _ButterFlyAssetVideo(), ], ), @@ -63,8 +62,7 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { _controller.addListener(() { setState(() {}); }); - _controller.initialize().then((_) => setState(() {})); - _controller.play(); + _controller.initialize().then((_) => _controller.play()); } @override @@ -156,6 +154,90 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } } +class _RtspRemoteVideo extends StatefulWidget { + @override + _RtspRemoteVideoState createState() => _RtspRemoteVideoState(); +} + +class _RtspRemoteVideoState extends State<_RtspRemoteVideo> { + MiniController? _controller; + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + Future changeUrl(String url) async { + if (_controller != null) { + await _controller!.dispose(); + } + + setState(() { + _controller = MiniController.network(url); + }); + + _controller!.addListener(() { + setState(() {}); + }); + + return _controller!.initialize(); + } + + String? _validateRtspUrl(String? value) { + if (value == null || !value.startsWith('rtsp://')) { + return 'Enter a valid RTSP URL'; + } + return null; + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Container(padding: const EdgeInsets.only(top: 20.0)), + const Text('With RTSP streaming'), + Padding( + padding: const EdgeInsets.all(20.0), + child: TextFormField( + autovalidateMode: AutovalidateMode.onUserInteraction, + decoration: const InputDecoration(label: Text('RTSP URL')), + validator: _validateRtspUrl, + textInputAction: TextInputAction.done, + onFieldSubmitted: (String value) { + if (_validateRtspUrl(value) == null) { + changeUrl(value); + } else { + setState(() { + _controller?.dispose(); + _controller = null; + }); + } + }, + ), + ), + if (_controller != null) + Container( + padding: const EdgeInsets.all(20), + child: AspectRatio( + aspectRatio: _controller!.value.aspectRatio, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + VideoPlayer(_controller!), + _ControlsOverlay(controller: _controller!), + VideoProgressIndicator(_controller!), + ], + ), + ), + ), + ], + ), + ); + } +} + class _ControlsOverlay extends StatelessWidget { const _ControlsOverlay({required this.controller}); diff --git a/packages/video_player/video_player_android/pubspec.yaml b/packages/video_player/video_player_android/pubspec.yaml index fee63491af753..ed629fa91f887 100644 --- a/packages/video_player/video_player_android/pubspec.yaml +++ b/packages/video_player/video_player_android/pubspec.yaml @@ -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.5.4 +version: 2.6.0 environment: sdk: ^3.4.0