---
title: "Read video metadata from a URL without downloading it in C#"
url: https://recursiveai.org/onemedia/cookbook/read-video-metadata-from-url/
description: "Read a remote video’s duration, codecs and tags in C# with HTTP range requests, fetching only the header bytes instead of downloading the whole MP4 or MKV file."
version: "0.1.0-preview.1"
---

# Read video metadata from a URL without downloading it in C#

`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).

```shell
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`:

```csharp
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");
```

```text
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.** `ReadRange` assumes a `206 Partial Content` answer. A server that ignores `Range` sends the whole file each time; check `response.StatusCode` if 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](https://recursiveai.org/onemedia/docs/isobmff/).
- **Matroska over HTTP** reads the elements up to the first Cluster. `Tags` and `Attachments` stored after the Clusters are found through the SeekHead and cost a read each, never a scan; each is fetched whole, up to `MediaLimits.MaxAttachmentBytes`.

The project’s byte and request counts for 213 files, over HTTP range requests and local reads, are in [docs/io-results.txt](https://github.com/recursive-ai-oss/onemedia-csharp/blob/v0.1.0-preview.1/docs/io-results.txt).
