Media.OpenAsync(length, readRange) reads a file through any function that returns a byte range, such as an HTTP range request, cloud blob storage or a browser Blob. Probing a file reads only its structure, so you get the duration, tracks, codecs and tags of a remote video for a small part of its size. The file here is the Sintel trailer on Blender’s server, 4.4 MB (CC BY 3.0, Blender Foundation).
dotnet new console -o RemoteProbe && cd RemoteProbe dotnet add package RecursiveAI.OneMedia --version 0.1.0-preview.1
Put this in Program.cs and run dotnet run:
using System.Net.Http.Headers; using RecursiveAI.OneMedia; string url = "https://download.blender.org/durian/trailer/sintel_trailer-480p.mp4"; using var http = new HttpClient(); // The length, from a HEAD request. using var head = await http.SendAsync(new HttpRequestMessage(HttpMethod.Head, url)); long length = head.Content.Headers.ContentLength ?? throw new InvalidOperationException("No Content-Length"); long fetched = 0; int requests = 0; async ValueTask<int> ReadRange(long offset, Memory<byte> buffer, CancellationToken ct) { using var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Range = new RangeHeaderValue(offset, offset + buffer.Length - 1); using var response = await http.SendAsync(request, ct); response.EnsureSuccessStatusCode(); byte[] bytes = await response.Content.ReadAsByteArrayAsync(ct); bytes.AsSpan(0, Math.Min(bytes.Length, buffer.Length)).CopyTo(buffer.Span); fetched += bytes.Length; requests++; return Math.Min(bytes.Length, buffer.Length); } await using MediaFile media = await Media.OpenAsync(length, ReadRange); var metadata = await media.ReadMetadataAsync(); Console.WriteLine($"Duration: {media.Duration}"); foreach (MediaTrack t in media.Tracks) Console.WriteLine($"Track {t.Index}: {t.Kind} {t.Codec}" + (t.Video is { } v ? $" {v.Width}x{v.Height}" : "")); Console.WriteLine($"Title: {metadata.Tags.Title}"); Console.WriteLine($"Fetched {fetched:N0} of {length:N0} bytes in {requests} requests");
Duration: 00:00:52.2090000 Track 0: Video h264 854x480 Track 1: Audio aac Title: Sintel Trailer Fetched 43,704 of 4,372,373 bytes in 7 requests
Over a ranged source, call ReadMetadataAsync() before reading media.Tags or the facts: the synchronous properties need a source they can read on demand. This file’s movie index is at its end, after 4.3 MB of media data (its first video packet starts at byte 48). OneMedia steps over the media data box by its header to reach the index, without reading the media in between. The project measured a 4.38 GB MP4 with the index at the end (local reads): 165,681 B in 34 reads, against 1,741,689 B in 32 reads for ffprobe and 23,628,223 B in 138 reads for MediaInfo.
Packets work over the same source with media.PacketsAsync(track). Each one is a request here, so for a whole track, read bigger ranges and cache them in your readRange.
Limits
- The server must honor ranges.
ReadRangeassumes a206 Partial Contentanswer. A server that ignoresRangesends the whole file each time; checkresponse.StatusCodeif you don’t control it. - No decoding. Only container structure and tags are read; nothing is decoded, and no thumbnail can be made.
- Fragmented MP4 durations come from the headers, not from scanning the fragments, which keeps a remote probe cheap but can differ from a full read. See MP4, MOV and 3GP.
- Matroska over HTTP reads the elements up to the first Cluster.
TagsandAttachmentsstored after the Clusters are found through the SeekHead and cost a read each, never a scan; each is fetched whole, up toMediaLimits.MaxAttachmentBytes.
The project’s byte and request counts for 213 files, over HTTP range requests and local reads, are in docs/io-results.txt.