P6: remote video and audio play through the proxy
- The media proxy streams ranged requests from the origin (passing the range on, never caching), downloads and caches whole files otherwise, and serves cached files with range support. A PeerTube video is never fetched whole for one viewer, and clients still never contact the remote host. - PeerTube's fragmented MP4 files inside an HLS entry are read as variants, so HLS-only instances play too. - A remote Video or Audio post becomes one playable Mastodon attachment: the best MP4 up to 720p that carries both sound and picture, with its poster and duration; the card is kept only when nothing is playable. - nginx: /media/proxy/ with proxy_buffering off and a 600 s read timeout (applied on the box, with a backup). Checked live: a GoToSocial image through the proxy answers 206 with exactly the asked range when streamed, 200 when cached, and 206 with the right Content-Range from the cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
This commit is contained in:
1 parent
6f1ab0073c
commit
f9658a8e7a
10 files changed
+193
-22
No files matched your search
@@ -60,13 +60,38 @@ namespace PrivaPub.Api.Mastodon.Controllers
|
||||
[HttpGet("/media/proxy/{signature}/{encoded}"), AllowAnonymous, ApiExplorerSettings(IgnoreApi = true)]
|
||||
public async Task<IActionResult> Proxy(string signature, string encoded, CancellationToken token)
|
||||
{
|
||||
var (path, contentType) = await _proxy.Fetch(signature, encoded, token);
|
||||
if (path == default)
|
||||
var url = _proxy.Verified(signature, encoded);
|
||||
if (url == default)
|
||||
return NotFound();
|
||||
Response.Headers["X-Content-Type-Options"] = "nosniff";
|
||||
Response.Headers["Content-Security-Policy"] = "default-src 'none'; sandbox";
|
||||
Response.Headers["Cache-Control"] = "public, max-age=604800";
|
||||
return PhysicalFile(path, contentType);
|
||||
if (_proxy.Cached(url) is { Path: not null } cached)
|
||||
return PhysicalFile(cached.Path, cached.ContentType, enableRangeProcessing: true);
|
||||
if (Request.Headers.Range.Count == 0)
|
||||
{
|
||||
var (path, contentType) = await _proxy.Fetch(signature, encoded, token);
|
||||
if (path != default)
|
||||
return PhysicalFile(path, contentType, enableRangeProcessing: true);
|
||||
}
|
||||
return await Stream(url, token);
|
||||
}
|
||||
|
||||
async Task<IActionResult> Stream(string url, CancellationToken token)
|
||||
{
|
||||
var range = System.Net.Http.Headers.RangeHeaderValue.TryParse(Request.Headers.Range.ToString(), out var asked) ? asked : default;
|
||||
using var upstream = await _proxy.Open(url, range, token);
|
||||
if (upstream == default)
|
||||
return NotFound();
|
||||
Response.StatusCode = (int)upstream.StatusCode;
|
||||
Response.ContentType = upstream.Content.Headers.ContentType?.ToString() ?? "application/octet-stream";
|
||||
if (upstream.Content.Headers.ContentLength is { } length)
|
||||
Response.ContentLength = length;
|
||||
if (upstream.Content.Headers.ContentRange is { } contentRange)
|
||||
Response.Headers.ContentRange = contentRange.ToString();
|
||||
Response.Headers.AcceptRanges = "bytes";
|
||||
await upstream.Content.CopyToAsync(Response.Body, token);
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
object View(MediaAttachment attachment) => new
|
||||
|
||||
@@ -225,7 +225,7 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
SpoilerText = post.SpoilerText ?? (post.HasContentWarning ? post.Title ?? ActivityPubRenderer.ContentWarning : string.Empty),
|
||||
Visibility = Visibility(post.Visibility),
|
||||
Language = post.Language,
|
||||
MediaAttachments = post.Media.Select(Media).ToList(),
|
||||
MediaAttachments = post.Media.Count > 0 ? post.Media.Select(Media).ToList() : Playable(post),
|
||||
Card = Card(post),
|
||||
Poll = Poll(post, viewerId, ownVotes.GetValueOrDefault(post.ID)),
|
||||
Emojis = Emojis(post.Emojis),
|
||||
@@ -445,11 +445,42 @@ namespace PrivaPub.Api.Mastodon.Mappers
|
||||
Streams = variant.Streams
|
||||
};
|
||||
|
||||
List<MediaAttachment> Playable(PostEntity post)
|
||||
{
|
||||
var (variant, kind, duration) = post.Video is { } video ? (BestVideo(video.Variants), "video", video.DurationSeconds)
|
||||
: post.Audio is { } audio ? (audio.Variants.FirstOrDefault(), "audio", audio.DurationSeconds)
|
||||
: default;
|
||||
if (variant == default)
|
||||
return new List<MediaAttachment>();
|
||||
return new List<MediaAttachment>
|
||||
{
|
||||
Media(new PostMedia
|
||||
{
|
||||
Id = new Guid(System.Security.Cryptography.MD5.HashData(System.Text.Encoding.UTF8.GetBytes(variant.Url))),
|
||||
RemoteURL = variant.Url,
|
||||
ContentType = variant.MediaType ?? kind + "/*",
|
||||
RemotePreviewURL = post.CoverURL,
|
||||
Width = variant.Width,
|
||||
Height = variant.Height,
|
||||
DurationSeconds = duration,
|
||||
Description = post.Title
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
public static MediaVariant BestVideo(IReadOnlyCollection<MediaVariant> variants)
|
||||
{
|
||||
var playable = variants.Where(v => v.MediaType?.StartsWith("video/") == true && (v.Streams.Count == 0 || v.Streams.Contains("audio") && v.Streams.Contains("video"))).ToList();
|
||||
return playable.Where(v => v.Height <= 720).OrderByDescending(v => v.Height).FirstOrDefault()
|
||||
?? playable.OrderBy(v => v.Height ?? int.MaxValue).FirstOrDefault();
|
||||
}
|
||||
|
||||
PreviewCard Card(PostEntity post)
|
||||
{
|
||||
if (post.Link is { } link)
|
||||
return Card(link.Href, link.Title ?? post.Title, link.Description ?? (post.Media.Count == 0 ? post.Excerpt : default), link.ImageURL);
|
||||
if (post.IsFederatedCopy && post.ObjectType is "Article" or "Event" or "Video" or "Audio" && post.Url != default && post.Media.Count == 0)
|
||||
if (post.IsFederatedCopy && post.ObjectType is "Article" or "Event" && post.Url != default && post.Media.Count == 0
|
||||
|| post.IsFederatedCopy && post.ObjectType is "Video" or "Audio" && post.Url != default && post.Media.Count == 0 && Playable(post).Count == 0)
|
||||
return Card(post.Url, post.Title, post.Excerpt, post.CoverURL);
|
||||
return default;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user