Visarc.Umbraco.Qencode.Core 17.1.12-dev.13

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.
StorageMode Cdn Cdn keeps the renditions on Qencode-side storage and records their absolute URLs. Local downloads them into the Umbraco media library instead — see Local storage mode.
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.
LocalDownloadTimeoutMinutes 10 Local mode only. Per-rendition download timeout.
LocalImportSweepMinutes 5 Local mode only. How often the recovery sweep looks for outputs that were never imported.
LocalImportMaxAttempts 3 Local mode only. Import attempts before the video is marked failed.

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

Local storage mode

Set Visarc:Qencode:StorageMode to Local for sites with no CDN of their own:

{ "Visarc": { "Qencode": { "StorageMode": "Local" } } }

Qencode still does the transcoding. The difference is where the outputs end up: every destination is stripped from the job query (including one a Qencode template supplies), so Qencode writes to its own temporary storage; the plugin then downloads each output and saves it into the media library as a child of the source video, and records the resulting /media/... URLs in exactly the properties CDN mode writes. Nothing on the read side changes — the player partial, the SEO helpers, .Url() and the backoffice all behave identically in either mode.

Cdn is the default, so an existing install that never sets StorageMode is unaffected.

The renditions

They are created as Umbraco's built-in File media type (video renditions and the generated .vtt) and Image (thumbnails), named after the source video — "Product tour 1080p", "Product tour 16x9 thumbnail", "Product tour subtitles". QencodeVideoMediaTypeReconciler allows those two types as children of the Video media type on startup, which Umbraco otherwise forbids.

Deliberately not the Video media type: a File item carries none of the Qencode properties, so there is no status, no "Process video" button and nothing for QencodeMediaUrlProvider to intercept. A rendition cannot be transcoded again because it has no way to express that it might be — no guard code, and no way to get it wrong.

The link back to the source is the media tree parentage, plus the source video's own qencodeLocalRenditions property, which records the keys of the items the plugin created. Don't edit or move the children by hand: they are regenerated wholesale, and the source video's recorded URLs are what the site actually renders.

Re-processing a video replaces its renditions. The old set is recycled only once the new one is in place, so the video keeps playing throughout.

How the import runs

Downloading happens in the background, never on the request thread:

  1. Qencode reports the job complete (via its callback, or the backoffice poll). The output URLs, and the fact an import is due, are written to qencodeLocalRenditions. qencodeStatus stays transcoding.
  2. The media key is queued and picked up by a background service, which downloads each output, creates the media items, writes the final URLs, and flips qencodeStatus to completed.
  3. QencodeLocalImportSweepJob — an Umbraco recurring job, scheduling publisher only — re-queues anything left pending. That is how a restart mid-import recovers.

Because an import is claimed on the media item itself, two overlapping completions (a callback and a poll arriving together, or a sweep overlapping a running import) cannot both start one. Queuing is restricted to the scheduling publisher, so a load-balanced site imports on one server only.

If an import can't be completed — too many failed attempts, or Qencode's temporary URLs expired before the files could be fetched — the video is marked failed with a readable error rather than sitting on "transcoding" forever, so an editor can re-process it.

One cosmetic note: the progress percentage shown in the backoffice is Qencode's own, so it stops moving during the download phase, which the UI still shows as "Transcoding…".

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 sealed class QencodeVideoPlayerModel
{
    public QencodeVideoPlayerModel() { }

    // Use this from Razor views. `Media` is a `required` member, and views are usually
    // compiled at runtime by a pre-C#11 Roslyn that rejects required-member object
    // initializers ("Constructors of types with required members are not supported in
    // this version of your compiler"). A [SetsRequiredMembers] ctor carries no such guard.
    [SetsRequiredMembers]
    public QencodeVideoPlayerModel(IPublishedContent media, bool showControls = true);

    public required IPublishedContent Media { get; init; }
    public bool ShowControls { get; init; } = 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, or in local storage mode the imported media item's /media/... path — 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.
  • Local storage mode adds IQencodeLocalImportQueue (Singleton), IQencodeLocalImportService (Scoped), a hosted service that drains the queue, a recurring sweep job, and a named HttpClient (QencodeLocalImportService.DownloadHttpClientName) with a long timeout for the downloads. All are registered unconditionally and no-op in CDN mode.
  • 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 QencodeTranscodingService — DetermineHasAudio, 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.
46
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
27
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
24
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
18
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
11
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
9
Visarc.Umbraco.Qencode
Qencode video transcoding integration for Umbraco CMS. Configure transcoding modes per video and serve transcoded output from CDN.
8
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.12-dev.19 1 9/22/2026
17.1.12-dev.18 1 9/22/2026
17.1.12-dev.17 1 9/22/2026
17.1.12-dev.16 1 9/22/2026
17.1.12-dev.15 1 9/22/2026
17.1.12-dev.13 1 9/21/2026
17.1.12-dev.12 1 9/21/2026
17.1.12-dev.11 2 9/17/2026
17.1.12-dev.10 24 9/15/2026
17.1.11 18 8/27/2026
17.1.10 27 8/25/2026
17.1.9 47 8/11/2026
17.1.8 8 8/10/2026
17.1.7 6 8/4/2026
17.1.6 6 8/4/2026
17.1.5 6 8/4/2026
17.1.4 6 7/31/2026
17.1.3 6 7/31/2026
17.1.2 11 7/28/2026
17.1.1 11 7/27/2026
17.1.0 6 7/27/2026
17.0.1 7 7/27/2026