Skip to content

Commit e443cf4

Browse files
committed
Added Stream as a media source for all platforms except Tizen
1 parent 87e176c commit e443cf4

10 files changed

Lines changed: 406 additions & 2 deletions

src/CommunityToolkit.Maui.MediaElement/Converters/MediaSourceConverter.shared.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destina
5454
UriMediaSource uriMediaSource => uriMediaSource.ToString(),
5555
FileMediaSource fileMediaSource => fileMediaSource.ToString(),
5656
ResourceMediaSource resourceMediaSource => resourceMediaSource.ToString(),
57+
StreamMediaSource streamMediaSource => streamMediaSource.ToString(),
5758
MediaSource => string.Empty,
5859
_ => throw new ArgumentException($"Invalid Media Source", nameof(value))
5960
};

src/CommunityToolkit.Maui.MediaElement/MediaSource/MediaSource.shared.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ internal event EventHandler SourceChanged
5757
/// <returns>A <see cref="FileMediaSource"/> instance.</returns>
5858
public static MediaSource FromFile(string? path) => new FileMediaSource { Path = path };
5959

60+
/// <summary>
61+
/// Creates a <see cref="StreamMediaSource"/> from a <see cref="Stream"/>.
62+
/// </summary>
63+
/// <param name="stream">The stream to use as a media source.</param>
64+
/// <returns>A <see cref="StreamMediaSource"/> instance.</returns>
65+
public static StreamMediaSource FromStream(Stream stream) => new StreamMediaSource { Stream = stream };
66+
6067
/// <summary>
6168
/// Creates a <see cref="UriMediaSource"/> from an absolute URI.
6269
/// </summary>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Microsoft.Maui.Controls;
2+
3+
namespace CommunityToolkit.Maui.Views;
4+
5+
/// <summary>
6+
/// Represents a source, loaded from a <see cref="System.IO.Stream"/>, that can be played by <see cref="MediaElement"/>.
7+
/// </summary>
8+
public sealed partial class StreamMediaSource : MediaSource
9+
{
10+
/// <summary>
11+
/// Backing store for the <see cref="Stream"/> property.
12+
/// </summary>
13+
public static readonly BindableProperty StreamProperty
14+
= BindableProperty.Create(nameof(Stream), typeof(Stream), typeof(StreamMediaSource), propertyChanged: OnStreamMediaSourceChanged);
15+
16+
/// <summary>
17+
/// An implicit operator to convert a <see cref="System.IO.Stream"/> value into a <see cref="StreamMediaSource"/>.
18+
/// </summary>
19+
/// <param name="stream">The stream to use as a media source.</param>
20+
public static implicit operator StreamMediaSource?(Stream? stream) => stream is not null ? FromStream(stream) : null;
21+
22+
/// <summary>
23+
/// An implicit operator to convert a <see cref="StreamMediaSource"/> into a <see cref="System.IO.Stream"/> value.
24+
/// </summary>
25+
/// <param name="streamMediaSource">A <see cref="StreamMediaSource"/> instance to convert to a <see cref="System.IO.Stream"/> value.</param>
26+
public static implicit operator Stream?(StreamMediaSource? streamMediaSource) => streamMediaSource?.Stream;
27+
28+
/// <summary>
29+
/// Gets or sets the stream to use as a media source.
30+
/// This is a bindable property.
31+
/// </summary>
32+
public Stream? Stream
33+
{
34+
get => (Stream?)GetValue(StreamProperty);
35+
set => SetValue(StreamProperty, value);
36+
}
37+
38+
/// <inheritdoc/>
39+
public override string ToString() => $"Stream: {Stream?.GetType().Name ?? "null"}";
40+
41+
static void OnStreamMediaSourceChanged(BindableObject bindable, object oldValue, object newValue) =>
42+
((StreamMediaSource)bindable).OnSourceChanged();
43+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using AndroidX.Media3.Common;
2+
using AndroidX.Media3.DataSource;
3+
4+
namespace CommunityToolkit.Maui.Core.Views;
5+
6+
/// <summary>
7+
/// A custom DataSource for ExoPlayer that wraps a .NET Stream.
8+
/// </summary>
9+
sealed class StreamDataSource : Java.Lang.Object, IDataSource
10+
{
11+
readonly Stream stream;
12+
readonly long length;
13+
Android.Net.Uri? uri;
14+
long bytesRemaining;
15+
16+
public StreamDataSource(Stream stream)
17+
{
18+
ArgumentNullException.ThrowIfNull(stream);
19+
20+
this.stream = stream;
21+
length = stream.CanSeek ? stream.Length : C.LengthUnset;
22+
bytesRemaining = 0;
23+
}
24+
25+
public void AddTransferListener(ITransferListener? transferListener)
26+
{
27+
// No-op: transfer listeners not supported for stream sources
28+
}
29+
30+
public Android.Net.Uri? Uri => uri;
31+
32+
public long Open(DataSpec? dataSpec)
33+
{
34+
if (dataSpec is null)
35+
{
36+
throw new ArgumentNullException(nameof(dataSpec));
37+
}
38+
39+
uri = dataSpec.Uri;
40+
41+
if (stream.CanSeek && dataSpec.Position > 0)
42+
{
43+
stream.Seek(dataSpec.Position, SeekOrigin.Begin);
44+
}
45+
46+
bytesRemaining = dataSpec.Length != C.LengthUnset
47+
? dataSpec.Length
48+
: (length != C.LengthUnset ? length - dataSpec.Position : C.LengthUnset);
49+
50+
return bytesRemaining;
51+
}
52+
53+
public int Read(byte[]? buffer, int offset, int length)
54+
{
55+
if (buffer is null)
56+
{
57+
throw new ArgumentNullException(nameof(buffer));
58+
}
59+
60+
if (length == 0)
61+
{
62+
return 0;
63+
}
64+
65+
if (bytesRemaining == 0)
66+
{
67+
return C.ResultEndOfInput;
68+
}
69+
70+
int bytesToRead = bytesRemaining != C.LengthUnset
71+
? (int)Math.Min(bytesRemaining, length)
72+
: length;
73+
74+
int bytesRead = stream.Read(buffer, offset, bytesToRead);
75+
76+
if (bytesRead == 0)
77+
{
78+
return C.ResultEndOfInput;
79+
}
80+
81+
if (bytesRemaining != C.LengthUnset)
82+
{
83+
bytesRemaining -= bytesRead;
84+
}
85+
86+
return bytesRead;
87+
}
88+
89+
public void Close()
90+
{
91+
uri = null;
92+
// Don't dispose the stream here - let the caller manage its lifetime
93+
}
94+
95+
protected override void Dispose(bool disposing)
96+
{
97+
if (disposing)
98+
{
99+
Close();
100+
}
101+
base.Dispose(disposing);
102+
}
103+
104+
public IDictionary<string, IList<string>>? ResponseHeaders => null;
105+
}
106+
107+
/// <summary>
108+
/// Factory for creating StreamDataSource instances.
109+
/// </summary>
110+
sealed class StreamDataSourceFactory : Java.Lang.Object, IDataSourceFactory
111+
{
112+
readonly Stream stream;
113+
114+
public StreamDataSourceFactory(Stream stream)
115+
{
116+
ArgumentNullException.ThrowIfNull(stream);
117+
this.stream = stream;
118+
}
119+
120+
public IDataSource CreateDataSource()
121+
{
122+
return new StreamDataSource(stream);
123+
}
124+
}

src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.android.cs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using AndroidX.Media3.Common;
99
using AndroidX.Media3.Common.Text;
1010
using AndroidX.Media3.Common.Util;
11+
using AndroidX.Media3.DataSource;
1112
using AndroidX.Media3.ExoPlayer;
1213
using AndroidX.Media3.Session;
1314
using AndroidX.Media3.UI;
@@ -39,6 +40,7 @@ public partial class MediaManager : Java.Lang.Object, IPlayerListener
3940
MediaSession? session;
4041
MediaItem.Builder? mediaItem;
4142
BoundServiceConnection? connection;
43+
StreamDataSourceFactory? currentStreamDataSourceFactory;
4244

4345
/// <summary>
4446
/// The platform native counterpart of <see cref="MediaElement"/>.
@@ -366,9 +368,19 @@ protected virtual async partial ValueTask PlatformUpdateSource()
366368
MediaElement.Duration = TimeSpan.Zero;
367369
MediaElement.CurrentStateChanged(MediaElementState.None);
368370

371+
currentStreamDataSourceFactory?.Dispose();
372+
currentStreamDataSourceFactory = null;
373+
369374
return;
370375
}
371376

377+
// Clear previous stream data source if switching sources
378+
if (MediaElement.Source is not StreamMediaSource)
379+
{
380+
currentStreamDataSourceFactory?.Dispose();
381+
currentStreamDataSourceFactory = null;
382+
}
383+
372384
MediaElement.CurrentStateChanged(MediaElementState.Opening);
373385
Player.PlayWhenReady = MediaElement.ShouldAutoPlay;
374386
cancellationTokenSource ??= new();
@@ -378,7 +390,18 @@ protected virtual async partial ValueTask PlatformUpdateSource()
378390

379391
if (item?.MediaMetadata is not null)
380392
{
381-
Player.SetMediaItem(item);
393+
// If we have a custom stream data source, we need to set the media source differently
394+
if (currentStreamDataSourceFactory is not null && MediaElement.Source is StreamMediaSource)
395+
{
396+
var mediaSource = new AndroidX.Media3.ExoPlayer.Source.ProgressiveMediaSource.Factory(currentStreamDataSourceFactory)
397+
.CreateMediaSource(item);
398+
Player.SetMediaSource(mediaSource);
399+
}
400+
else
401+
{
402+
Player.SetMediaItem(item);
403+
}
404+
382405
Player.Prepare();
383406
hasSetSource = true;
384407
}
@@ -541,6 +564,9 @@ protected override void Dispose(bool disposing)
541564
connection = null;
542565
}
543566

567+
currentStreamDataSourceFactory?.Dispose();
568+
currentStreamDataSourceFactory = null;
569+
544570
client.Dispose();
545571
}
546572
}
@@ -697,7 +723,7 @@ void StopService(in BoundServiceConnection boundServiceConnection)
697723

698724
break;
699725
}
700-
case ResourceMediaSource resourceMediaSource:
726+
case ResourceMediaSource resourceMediaSource:
701727
{
702728
var package = PlayerView?.Context?.PackageName ?? "";
703729
var path = resourceMediaSource.Path;
@@ -707,6 +733,15 @@ void StopService(in BoundServiceConnection boundServiceConnection)
707733
return await CreateMediaItem(assetFilePath, cancellationToken).ConfigureAwait(false);
708734
}
709735

736+
break;
737+
}
738+
case StreamMediaSource streamMediaSource:
739+
{
740+
if (streamMediaSource.Stream is not null)
741+
{
742+
return await CreateMediaItemFromStream(streamMediaSource.Stream, cancellationToken).ConfigureAwait(false);
743+
}
744+
710745
break;
711746
}
712747
default:
@@ -735,6 +770,30 @@ void StopService(in BoundServiceConnection boundServiceConnection)
735770
return mediaItem;
736771
}
737772

773+
async Task<MediaItem.Builder> CreateMediaItemFromStream(Stream stream, CancellationToken cancellationToken = default)
774+
{
775+
MediaMetadata.Builder mediaMetaData = new();
776+
mediaMetaData.SetArtist(MediaElement.MetadataArtist);
777+
mediaMetaData.SetTitle(MediaElement.MetadataTitle);
778+
var data = await GetBytesFromMetadataArtworkUrl(MediaElement.MetadataArtworkUrl, cancellationToken).ConfigureAwait(true);
779+
if (data is not null && data.Length > 0)
780+
{
781+
mediaMetaData.SetArtworkData(data, (Java.Lang.Integer)MediaMetadata.PictureTypeFrontCover);
782+
}
783+
784+
// Create MediaItem with metadata
785+
// The stream will be handled via custom data source factory when needed
786+
mediaItem = new MediaItem.Builder();
787+
mediaItem.SetUri("stream://media");
788+
mediaItem.SetMediaId("stream://media");
789+
mediaItem.SetMediaMetadata(mediaMetaData.Build());
790+
791+
// Store the stream for later use with custom MediaSource
792+
currentStreamDataSourceFactory = new StreamDataSourceFactory(stream);
793+
794+
return mediaItem;
795+
}
796+
738797
#region PlayerListener implementation method stubs
739798
public void OnAudioAttributesChanged(AudioAttributes? audioAttributes) { }
740799
public void OnAudioSessionIdChanged(int audioSessionId) { }

src/CommunityToolkit.Maui.MediaElement/Views/MediaManager.macios.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ namespace CommunityToolkit.Maui.Core.Views;
1414
public partial class MediaManager : IDisposable
1515
{
1616
Metadata? metaData;
17+
StreamAssetResourceLoader? streamResourceLoader;
1718

1819
// Media would still start playing when Speed was set although ShouldAutoPlay=False
1920
// This field was added to overcome that.
@@ -220,6 +221,13 @@ protected virtual async partial ValueTask PlatformUpdateSource()
220221
return;
221222
}
222223

224+
// Clean up previous stream resource loader if switching sources
225+
if (MediaElement.Source is not StreamMediaSource)
226+
{
227+
streamResourceLoader?.Dispose();
228+
streamResourceLoader = null;
229+
}
230+
223231
metaData ??= new(Player);
224232
Metadata.ClearNowPlaying();
225233
PlayerViewController?.ContentOverlayView?.Subviews.FirstOrDefault()?.RemoveFromSuperview();
@@ -260,6 +268,26 @@ protected virtual async partial ValueTask PlatformUpdateSource()
260268
Logger.LogWarning("Invalid file path for ResourceMediaSource.");
261269
}
262270
}
271+
else if (MediaElement.Source is StreamMediaSource streamMediaSource)
272+
{
273+
if (streamMediaSource.Stream is not null)
274+
{
275+
// Create a custom URL scheme for the stream
276+
var streamUrl = new NSUrl("stream://media");
277+
278+
// Create an AVURLAsset with the custom scheme
279+
var urlAsset = new AVUrlAsset(streamUrl);
280+
281+
// Create and set up the resource loader
282+
streamResourceLoader?.Dispose();
283+
streamResourceLoader = new StreamAssetResourceLoader(streamMediaSource.Stream, GetStreamContentType(streamMediaSource.Stream));
284+
285+
// Assign the resource loader delegate
286+
urlAsset.ResourceLoader.SetDelegate(streamResourceLoader, DispatchQueue.MainQueue);
287+
288+
asset = urlAsset;
289+
}
290+
}
263291

264292
PlayerItem = asset is not null
265293
? new AVPlayerItem(asset)
@@ -453,11 +481,24 @@ protected virtual void Dispose(bool disposing)
453481
Player = null;
454482
}
455483

484+
streamResourceLoader?.Dispose();
485+
streamResourceLoader = null;
486+
456487
PlayerViewController?.Dispose();
457488
PlayerViewController = null;
458489
}
459490
}
460491

492+
static string GetStreamContentType(Stream stream)
493+
{
494+
// Default to a generic media type
495+
// In a more sophisticated implementation, you could:
496+
// 1. Read magic bytes from the stream to detect type
497+
// 2. Accept content type as a parameter on StreamMediaSource
498+
// 3. Use file extension if available
499+
return "video/mp4"; // Default assumption
500+
}
501+
461502
static TimeSpan ConvertTime(CMTime cmTime) => TimeSpan.FromSeconds(double.IsNaN(cmTime.Seconds) ? 0 : cmTime.Seconds);
462503

463504
static async Task<(int Width, int Height)> GetVideoDimensions(AVPlayerItem avPlayerItem)

0 commit comments

Comments
 (0)