Visarc.Umbraco.Qencode.Core 17.0.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,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. |
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:
- 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), 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.- 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.
|
6 |
|
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)