Visarc.Umbraco.Qencode.Core 17.1.1

Visarc.Umbraco.Qencode.Core

Backend (server-side) package for the Visarc Qencode video transcoding integration for Umbraco 17.

This package contains everything that's safe to reference from your own code — models, rendering helpers, and the backend logic that talks to Qencode — with no dependency on the Umbraco backoffice UI:

  • Models/QencodeMediaStatus, QencodeVideoPlayerModel, VideoObjectJsonLd, QencodeCdnEntry, QencodeProcessOptions, QencodeTemplateSummary and the various Qencode API request/response DTOs.
  • Extensions/IPublishedContent/IMedia helpers: IsTranscodingComplete(), GetQencodeVideos(), GetQencodeStatus(), GetQencodePosterUrl(), GetQencodeSubtitlesUrl(), GetQencodeVideoPicker(), GetVideoObjectJsonLd() / GetVideoObjectJsonLdScript().
  • Services/IQencodeTranscodingService and the Qencode API clients that start jobs, poll status, and resolve saved transcoding templates.
  • Controllers/ — the backoffice Management API endpoints (process/re-process, status, templates, media-types/video) and the public Qencode completion callback.
  • Providers/ — a custom IMediaUrlProvider so Umbraco's own .Url() resolves to the Qencode CDN URL for completed videos.
  • Migrations/ — self-installing setup: creates the Video media type's qencode*/SEO properties and the two property editor data types on first run.
  • ValueConverters/ — converts the Qencode Video Picker's stored JSON into a strongly-typed QencodeVideoPickerValue for ModelsBuilder / Value<T>.
  • Composers/ — registers services, the value converter, the media URL provider, and the migration.

When to reference this package

Reference Visarc.Umbraco.Qencode.Core directly when you only need the backend — for example a separate Models/rendering project that doesn't host the Umbraco backoffice, which is the usual split for larger Umbraco solutions.

For a normal Umbraco website, install Visarc.Umbraco.Qencode instead. It references this package and additionally ships the backoffice property editors (Qencode Video Manager, Qencode Video Picker) and the QencodeVideoPlayer view.

Configuration

Bound from the Visarc:Qencode section in appsettings.json:

{
  "Visarc": {
    "Qencode": {
      "ApiKey": "your-qencode-api-key",
      "CallbackSecret": "a-long-random-string",
      "PrivateCdnDomain": "cdn.example.com",
      "ThumbnailHeight": 720
    }
  }
}
Setting Default Purpose
ApiKey (empty) Qencode API key. Required.
TranscodingQuery 1080p mp4 ([{"output":"mp4","size":"1920x1080","video_codec":"libx264"}]) Base format entry for the transcoding job; per-video modes (audio/subtitles/thumbnails) are appended at processing time.
BaseUrl https://api.qencode.com Qencode Transcoding API base URL.
AuthApiBaseUrl https://auth.qencode.com Used to exchange ApiKey for a bearer token.
AccountApiBaseUrl https://account-api.qencode.com Used to list/resolve saved transcoding templates.
PrivateCdnDomain (empty) Custom CDN hostname substituted into every returned URL (video/thumbnail/subtitles), e.g. a branded domain CNAME'd onto storage.
MediaTypeAliases (empty) Reserved for a future release — currently not enforced anywhere in this version. Do not rely on it to restrict which media types are processed.
FileExtensions (empty) Reserved for a future release — currently not enforced anywhere in this version. Do not rely on it to restrict which files are processed.
CallbackBaseUrl (empty) Override the base URL used when building the Qencode callback URL (e.g. behind a reverse proxy). Auto-detected from the request if empty.
CallbackSecret (empty) Shared secret appended to the callback URL query string; the callback endpoint rejects requests without a matching secret. Strongly recommended — see Callback webhook below.
SubtitleLanguage (empty) Language code for speech-to-text; auto-detected if empty.
SubtitleMode balanced Speech-to-text mode: speed, balanced or accuracy.
ThumbnailImageFormat jpg jpg or png.
ThumbnailHeight 720 Height in px for generated thumbnails (widths derived per aspect ratio: 1:1, 4:3, 16:9). Google requires a minimum of 112px.
ThumbnailTime 0.5 Default thumbnail capture point (fraction of duration, 0.0–1.0), when not overridden per-video from the Video Manager UI.

Processing is never triggered automatically on media save — only via the explicit "Process video" button (or a direct call to the process/{key} endpoint below).

Usage

Once a video has finished transcoding, its data is available directly from the media item:

@using Visarc.Umbraco.Qencode.Core.Extensions

@{
    var video = Umbraco.Media(mediaKey);
}

@if (video is not null && video.IsTranscodingComplete())
{
    var rendition = video.GetQencodeVideos()?.FirstOrDefault();
    <video src="@rendition?.Url" controls></video>
}

Content editors reference a video via the Qencode Video Picker property editor; resolve its value with the strongly-typed QencodeVideoPickerValue (see Visarc.Umbraco.Qencode's README for the full pattern, including the ready-made QencodeVideoPlayer partial and VideoObject JSON-LD).

Extension methods reference

All extend IPublishedContent and live in Visarc.Umbraco.Qencode.Core.Extensions:

// MediaExtensions
IReadOnlyList<TranscodedVideo>? media.GetQencodeVideos();     // renditions, ordered by height desc
string?                        media.GetQencodeStatus();      // "transcoding" / "completed" / "failed" / null
bool                           media.IsTranscodingComplete(); // status == "completed"

// VideoSeoExtensions
VideoObjectJsonLd? media.GetVideoObjectJsonLd();                          // null unless name/description/uploadDate/thumbnail are all set
IHtmlContent?      media.GetVideoObjectJsonLdScript();                    // ready-to-embed <script type="application/ld+json">
string?            media.GetQencodePosterUrl(string cropAlias = "16x9");  // manual poster (cropped) or best-matching auto-thumbnail
string?            media.GetQencodeSubtitlesUrl();                       // manual .vtt override or auto-generated VTT

// ContentExtensions
QencodeVideoPickerValue? content.GetQencodeVideoPicker(string propertyAlias); // manual read without ModelsBuilder

Models reference

public sealed class QencodeVideoPickerValue
{
    public string? MediaKey { get; set; }
    public bool ShowControls { get; set; } = true;
}

public class QencodeMediaStatus
{
    public string Status { get; set; } = "idle"; // idle / transcoding / completed / failed
    public string? Name { get; set; }
    public string? ThumbnailUrl { get; set; }
    public string? PrimaryUrl { get; set; }
    public string? Duration { get; set; }
    public double? Percent { get; set; }         // live progress only, not persisted
    public bool HasThumbnails { get; set; }
    public bool HasSubtitles { get; set; }
    public string? SubtitlesUrl { get; set; }
    public bool? HasAudio { get; set; }          // see "HasAudio" note below
    public string? ErrorMessage { get; set; }    // only meaningful when Status == "failed"
}

public class QencodeVideoPlayerModel
{
    public required IPublishedContent Media { get; set; }
    public bool ShowControls { get; set; } = true;
}

public class VideoObjectJsonLd
{
    public string Context { get; } = "https://schema.org"; // "@context"
    public string Type { get; } = "VideoObject";            // "@type"
    public required string Name { get; set; }
    public required string Description { get; set; }
    public required IReadOnlyList<string> ThumbnailUrl { get; set; }
    public required DateTime UploadDate { get; set; }
    public string? Duration { get; set; }     // ISO 8601, e.g. "PT30S"
    public string? ContentUrl { get; set; }
}

public class QencodeCdnEntry
{
    public int Width { get; set; }
    public int Height { get; set; }
    public string Url { get; set; } = "";
    public string Kind { get; set; } = "video"; // "video" for renditions, or a thumbnail user_tag e.g. "visarc-thumb-1x1"
}

// Sent by the Video Manager UI to POST process/{key}
public class QencodeProcessOptions
{
    public bool GenerateSubtitles { get; set; }
    public bool GenerateThumbnails { get; set; }
    public int ThumbnailPercent { get; set; } = 50;
    public string? TemplateId { get; set; }
}

public class QencodeTemplateSummary
{
    public required string Id { get; set; }
    public string? Name { get; set; }
    public string? Description { get; set; }
}

HasAudio

qencodeHasAudio is derived once, at the moment processing starts, from the resolved transcoding query's audio_mute flag on its primary mp4 format entry (e.g. a "no audio" template sets audio_mute, a "with audio" template doesn't). It is null/unset only when:

  • the video was processed before this property existed, or
  • the resolved query has no recognizable mp4 format entry to inspect.

Treat null in the UI/API as "unknown", not "no audio" — the Video Picker's status badge does exactly that.

Backend Management API

All under /umbraco/management/api/v1/visarc/qencode/, backoffice-authenticated, served from their own Swagger document (ApiName = "qencode", at /umbraco/swagger/qencode/swagger.json) rather than Umbraco's main API document:

Route Method Purpose Notable responses
media-types/video GET Returns { "id": "<guid>" } — the Video media type's GUID, for filtering media pickers 404 if the Video media type doesn't exist
templates?search= GET Lists saved Qencode transcoding templates (QencodeTemplateSummary[]) 502 if the Account API call fails
status/{key:guid} GET Current QencodeMediaStatus for the media item with this key 404 if media not found
process/{key:guid} POST Starts/re-starts transcoding; body is QencodeProcessOptions 202 Started · 409 already in progress · 422 no source file · 400 not configured (missing ApiKey) · 502 Qencode API error or template fetch failed

Callback webhook

POST /api/qencode/callback/{mediaId}?secret=... is a plain, public route (not the versioned Management API, no Umbraco backoffice auth) — this is the URL Qencode itself calls when a job finishes. It's protected only by:

  1. The secret query-string parameter, checked against CallbackSecret. If CallbackSecret is empty, this check is skipped entirely and a warning is logged — set it in production.
  2. The callback body's task_token must match the media item's stored qencodeTaskToken.

Make sure this route is reachable from the public internet (Qencode needs to call it), and set CallbackBaseUrl if the auto-detected request URL doesn't match your public-facing host (e.g. behind a reverse proxy or when running locally with a tunnel).

Frontend media URLs

QencodeComposer registers a custom IMediaUrlProvider, QencodeMediaUrlProvider. For any media item whose content type alias contains "video" (case-insensitive) it always handles the URL resolution itself — it never returns null, so Umbraco's built-in DefaultMediaUrlProvider never gets a chance to fall back to the local uploaded file path. Concretely, calling .Url() on a Video item resolves to:

  • qencodePrimaryUrl (the Qencode CDN URL), once qencodeStatus == "completed"
  • QencodeMediaUrlProvider.NotEncodedUrl (the literal string "error:qencode-video-not-yet-encoded") otherwise

This is intentional: it stops the raw, unoptimized local media file from ever being exposed by .Url() just because a video hasn't finished transcoding yet. If you're calling .Url() directly instead of using the extension methods above, guard it:

var url = video.IsTranscodingComplete() ? video.Url(Umbraco) : null;

DI registration notes

  • IQencodeQueryBuilder is registered Singleton; IQencodeTranscodingService is Scoped — worth knowing if you want to decorate or override either.
  • Three typed HttpClients are registered: IQencodeApiClient (transcoding), IQencodeAuthApiClient (token exchange), IQencodeAccountApiClient (templates).
  • Migrations self-install via a UmbracoApplicationStartedNotification handler — no manual migration step is needed.

Testing

Visarc.Umbraco.Qencode.Core.Tests (xUnit) covers the dependency-free logic that's safe to unit test without a running Umbraco instance:

  • QencodeQueryBuilder — base format parsing, subtitle/thumbnail entry construction, destination rewriting/suffixing, template-override merging (including that it doesn't mutate the original template).
  • The pure helpers inside QencodeTranscodingServiceDetermineHasAudio, ParseHasAudio, FormatErrorMessage (exposed as internal via InternalsVisibleTo specifically so tests can reach them directly).
dotnet test "src/Visarc.Umbraco.Qencode.Core.Tests/Visarc.Umbraco.Qencode.Core.Tests.csproj"

Not covered yet: the IMedia/IPublishedContent-touching code (QencodeTranscodingService.StartAsync/GetStatusAsync, the extension methods, the migrations) — these need Umbraco's published-content fallback/value-converter machinery to test meaningfully rather than a plain mock, which is a larger effort left for a follow-up.

Compatibility

Package Version
Umbraco CMS 17.x ([17.0.0,18.0.0))
.NET 10.0

Showing the top 20 packages that depend on Visarc.Umbraco.Qencode.Core.

Packages Downloads
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
6
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
2
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
1

.NET 10.0

Version Downloads Last updated
17.1.2 2 7/28/2026
17.1.1 6 7/27/2026
17.1.0 1 7/27/2026
17.0.1 2 7/27/2026