Skip to content

Commit 99e8606

Browse files
authored
[video_player_android] Add RTSP support (flutter#7081)
Add RTSP support to `DataSourceType.network` videos on Android platform. I'm using this patch on my projects and it works well, but I need some feedback if the approach used is correct. If so, I will continue writing the tests. This PR implements the Android part of this feature request: flutter#18061 . I added a RTSP tab on the example app: https://github.com/flutter/packages/assets/7874200/9f0addb1-f6bb-4ec6-b8ad-e889f7d8b154
1 parent e3b8127 commit 99e8606

File tree

10 files changed

+171
-19
lines changed

10 files changed

+171
-19
lines changed

packages/video_player/video_player_android/AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,4 @@ Anton Borries <[email protected]>
6565
6666
Rahul Raj <[email protected]>
6767
Márton Matuz <[email protected]>
68+
André Sousa <[email protected]>

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.6.0
2+
3+
* Adds RTSP support.
4+
15
## 2.5.4
26

37
* Updates Media3-ExoPlayer to 1.4.0.

packages/video_player/video_player_android/android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ android {
5252
implementation "androidx.media3:media3-exoplayer:${exoplayer_version}"
5353
implementation "androidx.media3:media3-exoplayer-hls:${exoplayer_version}"
5454
implementation "androidx.media3:media3-exoplayer-dash:${exoplayer_version}"
55+
implementation "androidx.media3:media3-exoplayer-rtsp:${exoplayer_version}"
5556
implementation "androidx.media3:media3-exoplayer-smoothstreaming:${exoplayer_version}"
5657
testImplementation 'junit:junit:4.13.2'
5758
testImplementation 'androidx.test:core:1.3.0'
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919
import androidx.media3.exoplayer.source.MediaSource;
2020
import java.util.Map;
2121

22-
final class RemoteVideoAsset extends VideoAsset {
22+
final class HttpVideoAsset extends VideoAsset {
2323
private static final String DEFAULT_USER_AGENT = "ExoPlayer";
2424
private static final String HEADER_USER_AGENT = "User-Agent";
2525

2626
@NonNull private final StreamingFormat streamingFormat;
2727
@NonNull private final Map<String, String> httpHeaders;
2828

29-
RemoteVideoAsset(
29+
HttpVideoAsset(
3030
@Nullable String assetUrl,
3131
@NonNull StreamingFormat streamingFormat,
3232
@NonNull Map<String, String> httpHeaders) {
@@ -79,8 +79,8 @@ MediaSource.Factory getMediaSourceFactory(
7979
userAgent = httpHeaders.get(HEADER_USER_AGENT);
8080
}
8181
unstableUpdateDataSourceFactory(initialFactory, httpHeaders, userAgent);
82-
DataSource.Factory dataSoruceFactory = new DefaultDataSource.Factory(context, initialFactory);
83-
return new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSoruceFactory);
82+
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(context, initialFactory);
83+
return new DefaultMediaSourceFactory(context).setDataSourceFactory(dataSourceFactory);
8484
}
8585

8686
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.videoplayer;
6+
7+
import android.content.Context;
8+
import androidx.annotation.NonNull;
9+
import androidx.annotation.OptIn;
10+
import androidx.media3.common.MediaItem;
11+
import androidx.media3.common.util.UnstableApi;
12+
import androidx.media3.exoplayer.rtsp.RtspMediaSource;
13+
import androidx.media3.exoplayer.source.MediaSource;
14+
15+
final class RtspVideoAsset extends VideoAsset {
16+
RtspVideoAsset(@NonNull String assetUrl) {
17+
super(assetUrl);
18+
}
19+
20+
@NonNull
21+
@Override
22+
MediaItem getMediaItem() {
23+
return new MediaItem.Builder().setUri(assetUrl).build();
24+
}
25+
26+
// TODO: Migrate to stable API, see https://github.com/flutter/flutter/issues/147039.
27+
@OptIn(markerClass = UnstableApi.class)
28+
@Override
29+
MediaSource.Factory getMediaSourceFactory(Context context) {
30+
return new RtspMediaSource.Factory();
31+
}
32+
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,21 @@ static VideoAsset fromRemoteUrl(
4141
@Nullable String remoteUrl,
4242
@NonNull StreamingFormat streamingFormat,
4343
@NonNull Map<String, String> httpHeaders) {
44-
return new RemoteVideoAsset(remoteUrl, streamingFormat, new HashMap<>(httpHeaders));
44+
return new HttpVideoAsset(remoteUrl, streamingFormat, new HashMap<>(httpHeaders));
45+
}
46+
47+
/**
48+
* Returns an asset from a RTSP URL.
49+
*
50+
* @param rtspUrl remote asset, beginning with {@code rtsp://}.
51+
* @return the asset.
52+
*/
53+
@NonNull
54+
static VideoAsset fromRtspUrl(@NonNull String rtspUrl) {
55+
if (!rtspUrl.startsWith("rtsp://")) {
56+
throw new IllegalArgumentException("rtspUrl must start with 'rtsp://'");
57+
}
58+
return new RtspVideoAsset(rtspUrl);
4559
}
4660

4761
@Nullable protected final String assetUrl;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ public void initialize() {
110110
assetLookupKey = flutterState.keyForAsset.get(arg.getAsset());
111111
}
112112
videoAsset = VideoAsset.fromAssetUrl("asset:///" + assetLookupKey);
113+
} else if (arg.getUri().startsWith("rtsp://")) {
114+
videoAsset = VideoAsset.fromRtspUrl(arg.getUri());
113115
} else {
114116
Map<String, String> httpHeaders = arg.getHttpHeaders();
115117
VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN;

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ public void remoteVideoByDefaultSetsUserAgentAndCrossProtocolRedirects() {
6969

7070
DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
7171

72-
// Cast to RemoteVideoAsset to call a testing-only method to intercept calls.
73-
((RemoteVideoAsset) asset)
72+
// Cast to HttpVideoAsset to call a testing-only method to intercept calls.
73+
((HttpVideoAsset) asset)
7474
.getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
7575

7676
verify(mockFactory).setUserAgent("ExoPlayer");
@@ -89,8 +89,8 @@ public void remoteVideoOverridesUserAgentIfProvided() {
8989

9090
DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
9191

92-
// Cast to RemoteVideoAsset to call a testing-only method to intercept calls.
93-
((RemoteVideoAsset) asset)
92+
// Cast to HttpVideoAsset to call a testing-only method to intercept calls.
93+
((HttpVideoAsset) asset)
9494
.getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
9595

9696
verify(mockFactory).setUserAgent("FantasticalVideoBot");
@@ -127,12 +127,28 @@ public void remoteVideoSetsAdditionalHttpHeadersIfProvided() {
127127

128128
DefaultHttpDataSource.Factory mockFactory = mockHttpFactory();
129129

130-
// Cast to RemoteVideoAsset to call a testing-only method to intercept calls.
131-
((RemoteVideoAsset) asset)
130+
// Cast to HttpVideoAsset to call a testing-only method to intercept calls.
131+
((HttpVideoAsset) asset)
132132
.getMediaSourceFactory(ApplicationProvider.getApplicationContext(), mockFactory);
133133

134134
verify(mockFactory).setUserAgent("ExoPlayer");
135135
verify(mockFactory).setAllowCrossProtocolRedirects(true);
136136
verify(mockFactory).setDefaultRequestProperties(headers);
137137
}
138+
139+
@Test
140+
public void rtspVideoRequiresRtspUrl() {
141+
assertThrows(
142+
IllegalArgumentException.class, () -> VideoAsset.fromRtspUrl("https://not.rtsp/video.mp4"));
143+
}
144+
145+
@Test
146+
public void rtspVideoCreatesMediaItem() {
147+
VideoAsset asset = VideoAsset.fromRtspUrl("rtsp://test:[email protected]/stream");
148+
MediaItem mediaItem = asset.getMediaItem();
149+
150+
assert mediaItem.localConfiguration != null;
151+
assertEquals(
152+
mediaItem.localConfiguration.uri, Uri.parse("rtsp://test:[email protected]/stream"));
153+
}
138154
}

packages/video_player/video_player_android/example/lib/main.dart

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,24 @@ class _App extends StatelessWidget {
2020
@override
2121
Widget build(BuildContext context) {
2222
return DefaultTabController(
23-
length: 2,
23+
length: 3,
2424
child: Scaffold(
2525
key: const ValueKey<String>('home_page'),
2626
appBar: AppBar(
2727
title: const Text('Video player example'),
2828
bottom: const TabBar(
2929
isScrollable: true,
3030
tabs: <Widget>[
31-
Tab(
32-
icon: Icon(Icons.cloud),
33-
text: 'Remote',
34-
),
31+
Tab(icon: Icon(Icons.cloud), text: 'Remote'),
32+
Tab(icon: Icon(Icons.videocam), text: 'RTSP'),
3533
Tab(icon: Icon(Icons.insert_drive_file), text: 'Asset'),
3634
],
3735
),
3836
),
3937
body: TabBarView(
4038
children: <Widget>[
4139
_BumbleBeeRemoteVideo(),
40+
_RtspRemoteVideo(),
4241
_ButterFlyAssetVideo(),
4342
],
4443
),
@@ -63,8 +62,7 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> {
6362
_controller.addListener(() {
6463
setState(() {});
6564
});
66-
_controller.initialize().then((_) => setState(() {}));
67-
_controller.play();
65+
_controller.initialize().then((_) => _controller.play());
6866
}
6967

7068
@override
@@ -156,6 +154,90 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> {
156154
}
157155
}
158156

157+
class _RtspRemoteVideo extends StatefulWidget {
158+
@override
159+
_RtspRemoteVideoState createState() => _RtspRemoteVideoState();
160+
}
161+
162+
class _RtspRemoteVideoState extends State<_RtspRemoteVideo> {
163+
MiniController? _controller;
164+
165+
@override
166+
void dispose() {
167+
_controller?.dispose();
168+
super.dispose();
169+
}
170+
171+
Future<void> changeUrl(String url) async {
172+
if (_controller != null) {
173+
await _controller!.dispose();
174+
}
175+
176+
setState(() {
177+
_controller = MiniController.network(url);
178+
});
179+
180+
_controller!.addListener(() {
181+
setState(() {});
182+
});
183+
184+
return _controller!.initialize();
185+
}
186+
187+
String? _validateRtspUrl(String? value) {
188+
if (value == null || !value.startsWith('rtsp://')) {
189+
return 'Enter a valid RTSP URL';
190+
}
191+
return null;
192+
}
193+
194+
@override
195+
Widget build(BuildContext context) {
196+
return SingleChildScrollView(
197+
child: Column(
198+
children: <Widget>[
199+
Container(padding: const EdgeInsets.only(top: 20.0)),
200+
const Text('With RTSP streaming'),
201+
Padding(
202+
padding: const EdgeInsets.all(20.0),
203+
child: TextFormField(
204+
autovalidateMode: AutovalidateMode.onUserInteraction,
205+
decoration: const InputDecoration(label: Text('RTSP URL')),
206+
validator: _validateRtspUrl,
207+
textInputAction: TextInputAction.done,
208+
onFieldSubmitted: (String value) {
209+
if (_validateRtspUrl(value) == null) {
210+
changeUrl(value);
211+
} else {
212+
setState(() {
213+
_controller?.dispose();
214+
_controller = null;
215+
});
216+
}
217+
},
218+
),
219+
),
220+
if (_controller != null)
221+
Container(
222+
padding: const EdgeInsets.all(20),
223+
child: AspectRatio(
224+
aspectRatio: _controller!.value.aspectRatio,
225+
child: Stack(
226+
alignment: Alignment.bottomCenter,
227+
children: <Widget>[
228+
VideoPlayer(_controller!),
229+
_ControlsOverlay(controller: _controller!),
230+
VideoProgressIndicator(_controller!),
231+
],
232+
),
233+
),
234+
),
235+
],
236+
),
237+
);
238+
}
239+
}
240+
159241
class _ControlsOverlay extends StatelessWidget {
160242
const _ControlsOverlay({required this.controller});
161243

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.5.4
5+
version: 2.6.0
66

77
environment:
88
sdk: ^3.4.0

0 commit comments

Comments
 (0)