Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/video_player/video_player_android/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ Anton Borries <[email protected]>
Alex Li <[email protected]>
Rahul Raj <[email protected]>
Márton Matuz <[email protected]>
André Sousa <[email protected]>
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.6.0

* Adds RTSP support.

## 2.5.4

* Updates Media3-ExoPlayer to 1.4.0.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> httpHeaders;

RemoteVideoAsset(
HttpVideoAsset(
@Nullable String assetUrl,
@NonNull StreamingFormat streamingFormat,
@NonNull Map<String, String> httpHeaders) {
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,21 @@ static VideoAsset fromRemoteUrl(
@Nullable String remoteUrl,
@NonNull StreamingFormat streamingFormat,
@NonNull Map<String, String> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> httpHeaders = arg.getHttpHeaders();
VideoAsset.StreamingFormat streamingFormat = VideoAsset.StreamingFormat.UNKNOWN;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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:[email protected]/stream");
MediaItem mediaItem = asset.getMediaItem();

assert mediaItem.localConfiguration != null;
assertEquals(
mediaItem.localConfiguration.uri, Uri.parse("rtsp://test:[email protected]/stream"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,24 @@ class _App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
length: 3,
child: Scaffold(
key: const ValueKey<String>('home_page'),
appBar: AppBar(
title: const Text('Video player example'),
bottom: const TabBar(
isScrollable: true,
tabs: <Widget>[
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'),
],
),
),
body: TabBarView(
children: <Widget>[
_BumbleBeeRemoteVideo(),
_RtspRemoteVideo(),
_ButterFlyAssetVideo(),
],
),
Expand All @@ -63,8 +62,7 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> {
_controller.addListener(() {
setState(() {});
});
_controller.initialize().then((_) => setState(() {}));
_controller.play();
_controller.initialize().then((_) => _controller.play());
}

@override
Expand Down Expand Up @@ -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<void> 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: <Widget>[
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: <Widget>[
VideoPlayer(_controller!),
_ControlsOverlay(controller: _controller!),
VideoProgressIndicator(_controller!),
],
),
),
),
],
),
);
}
}

class _ControlsOverlay extends StatelessWidget {
const _ControlsOverlay({required this.controller});

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

environment:
sdk: ^3.4.0
Expand Down