Visarc.Umbraco.Qencode.Core 17.1.12-dev.15
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,QencodeTemplateSummaryand the various Qencode API request/response DTOs.Extensions/—IPublishedContent/IMediahelpers:IsTranscodingComplete(),GetQencodeVideos(),GetQencodeStatus(),GetQencodePosterUrl(),GetQencodeSubtitlesUrl(),GetQencodeVideoPicker(),GetVideoObjectJsonLd()/GetVideoObjectJsonLdScript().Services/—IQencodeTranscodingServiceand 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 customIMediaUrlProviderso Umbraco's own.Url()resolves to the Qencode CDN URL for completed videos.Migrations/— self-installing setup: creates the Video media type'sqencode*/SEO properties and the two property editor data types on first run.ValueConverters/— converts the Qencode Video Picker's stored JSON into a strongly-typedQencodeVideoPickerValuefor 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:
- 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.qencodeStatusstaystranscoding. - 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
qencodeStatustocompleted. 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:
- The
secretquery-string parameter, checked againstCallbackSecret. IfCallbackSecretis empty, this check is skipped entirely and a warning is logged — set it in production. - The callback body's
task_tokenmust match the media item's storedqencodeTaskToken.
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 — onceqencodeStatus == "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
IQencodeQueryBuilderis registered Singleton;IQencodeTranscodingServiceis 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 namedHttpClient(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
UmbracoApplicationStartedNotificationhandler — 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 asinternalviaInternalsVisibleTospecifically 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
- Umbraco.Cms.Api.Management (>= 17.0.0 && < 18.0.0)
- Umbraco.Cms.Core (>= 17.0.0 && < 18.0.0)
- Umbraco.Cms.Infrastructure (>= 17.0.0 && < 18.0.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 |