Plugin SDK Developer Guide¶
This guide covers everything needed to create, build, test, and distribute a third-party Decode-Orc stage plugin using the public Plugin SDK. No changes to the Decode-Orc source repository are required.
Quick Start¶
First install the SDK package from a decode-orc checkout (see
Obtaining the SDK below), then build a plugin against
it. The smallest complete, in-repo example is the CI fixture plugin at
orc-tests/fixtures/external-stage-plugin/ — three files (CMakeLists.txt,
plugin.cpp, one stage header) that configure, build, and load exactly as an
external plugin:
cmake -S my-orc-plugin -B my-orc-plugin/build \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=/path/to/sdk-prefix
cmake --build my-orc-plugin/build -j
Alternatively, clone the official skeleton template for a scaffold with unit tests and CI workflows (note: the skeleton repository may lag the SDK — the in-repo fixture above is always in sync with the installed package):
SDK Headers¶
Most plugin code needs only the public SDK umbrella header:
(The pre-tier spelling <orc/plugin/orc_plugin_sdk.h> still works via a
deprecated forwarding shim — see Deprecated pre-tier include paths below.)
Tier model¶
The SDK surface is split into three tiers with distinct stability promises:
orc/abi/— the frozen binary contract: descriptor, entrypoints, registration, and service tables. Any change here bumps the host ABI version. (Theorc/plugin/plugin-API headers are treated as part of this ABI surface until they are relocated under a tier.)orc/stage/— the stage contract: interfaces and data types that cross the plugin boundary, grouped by domain (preview/,observation/,dropout/,audio/,params/) with the foundation types at the tier root. A layout change here bumps the host ABI.orc/support/— compiled-into-plugin utilities. Explicitly not ABI: changes never require a bump, only a plugin recompile at the author's leisure.
The complete tiered header surface is generated from the single-source
manifest orc/sdk/sdk_headers.yaml:
orc/abi/ — frozen binary contract¶
Stability: any change bumps the host ABI version. Descriptor, entrypoints, registration, and service tables.
| Header | Provides |
|---|---|
<orc/abi/orc_plugin_abi.h> |
Host ABI contract for decode-orc stage plugins |
<orc/abi/orc_plugin_registration.h> |
Descriptor and entrypoint boilerplate helpers for stage plugins |
<orc/abi/orc_plugin_sdk.h> |
Umbrella include for the decode-orc stage plugin SDK |
<orc/abi/orc_plugin_services.h> |
Host service table injected into plugins at registration time. |
orc/plugin/ — plugin API surface (transitional)¶
Plugin-facing stage API and host-services headers not yet relocated under a tier subdirectory. Treated as ABI: layout changes bump the host ABI.
| Header | Provides |
|---|---|
<orc/plugin/orc_plugin_services_helpers.h> |
Helper macros for using OrcPluginServices. |
<orc/plugin/orc_stage_api.h> |
Stage API — stable types and interfaces for stage plugin |
<orc/plugin/orc_stage_preview.h> |
Public preview capability include surface for plugin stage |
<orc/plugin/orc_stage_runtime.h> |
Public stage runtime include surface for plugin stage |
<orc/plugin/orc_stage_services.h> |
Stable stage service interfaces exposed via OrcPluginServices |
<orc/plugin/orc_stage_tooling.h> |
Canonical stage helper/tooling contracts for plugin stages |
orc/stage/ — stage contract¶
Stage interfaces and data-contract types that cross the plugin boundary, grouped by domain. A layout change here bumps the host ABI version.
foundation
| Header | Provides |
|---|---|
<orc/stage/analysis_sink_results.h> |
Interfaces for accessing analysis sink stage results across |
<orc/stage/artifact.h> |
Artifact implementation |
<orc/stage/common_types.h> |
Common type definitions shared across all layers |
<orc/stage/cvbs_signal_constants.h> |
Normative CVBS_U10_4FSC signal constants for PAL, NTSC, and |
<orc/stage/error_types.h> |
Shared exception types for error classification |
<orc/stage/field_id.h> |
Field identifier implementation |
<orc/stage/file_io_interface.h> |
Interface(s) for file I/O to make unit testing easier |
<orc/stage/frame_descriptor.h> |
Per-frame metadata descriptor for CVBS_U10_4FSC frames |
<orc/stage/frame_id.h> |
Frame identifier types for CVBS_U10_4FSC frame-based pipeline |
<orc/stage/node_id.h> |
NodeID type definition for DAG nodes |
<orc/stage/node_type.h> |
Node type registry |
<orc/stage/orc_source_parameters.h> |
Source metadata types |
<orc/stage/stage.h> |
Base interface for all stage types |
<orc/stage/triggerable_stage.h> |
Triggerable interface for stages that can be manually executed |
<orc/stage/video_frame_representation.h> |
VideoFrameRepresentation interface for CVBS_U10_4FSC frames |
<orc/stage/video_metadata_types.h> |
Video metadata types exposed through VFR interface |
audio
| Header | Provides |
|---|---|
<orc/stage/audio/audio_channel_pair.h> |
Audio channel-pair model shared by all |
dropout
| Header | Provides |
|---|---|
<orc/stage/dropout/dropout_decision.h> |
Dropout decision management |
<orc/stage/dropout/dropout_run.h> |
Frame-flat dropout descriptor for CVBS_U10_4FSC frames |
observation
| Header | Provides |
|---|---|
<orc/stage/observation/observation_context.h> |
Pipeline-scoped observation storage |
<orc/stage/observation/observation_context_interface.h> |
Pipeline-scoped observation storage |
<orc/stage/observation/observation_schema.h> |
Observation schema definitions |
<orc/stage/observation/observation_service_interface.h> |
Host-owned observation service reached via OrcPluginServices |
params
| Header | Provides |
|---|---|
<orc/stage/params/parameter_types.h> |
Stage parameter type definitions shared across all layers |
<orc/stage/params/stage_parameter.h> |
Stage Parameter |
preview
| Header | Provides |
|---|---|
<orc/stage/preview/colour_preview_provider.h> |
Interface for stages exposing colour-domain preview carriers. |
<orc/stage/preview/orc_preview_carriers.h> |
Typed preview carriers used by the Phase 2 preview pipeline. |
<orc/stage/preview/orc_preview_types.h> |
Shared preview taxonomy: video data types, colorimetric |
<orc/stage/preview/orc_rendering.h> |
Public API for rendering and preview types |
<orc/stage/preview/orc_vectorscope.h> |
Public API for vectorscope visualization data |
<orc/stage/preview/preview_stage_types.h> |
Shared lightweight types for stage preview interfaces |
<orc/stage/preview/stage_custom_preview_renderer.h> |
Interface for stages with non-standard preview rendering. |
<orc/stage/preview/stage_preview_capability.h> |
Capability contracts for stages that expose structured preview |
orc/support/ — compiled-into-plugin utilities¶
NOT part of the binary ABI. Changes never force an ABI bump; recompile the plugin at the author's convenience.
| Header | Provides |
|---|---|
<orc/support/colour_preview_conversion.h> |
Render-boundary conversion from colour carriers to PreviewImage. |
<orc/support/dropout_util.h> |
Frame-flat ↔ field/line/sample coordinate conversion utilities |
<orc/support/eia608_decoder.h> |
EIA-608 Closed Caption Decoder for timed text conversion |
<orc/support/frame_line_util.h> |
Per-line sample count and offset helpers for 4FSC CVBS flat |
<orc/support/logging.h> |
Logging system implementation |
<orc/support/lru_cache.h> |
Thread-safe least-recently-used cache |
<orc/support/preview_helpers.h> |
Helper functions for stage preview rendering |
<orc/support/stage_instructions.h> |
Runtime loader for a stage's instructions.md (platform file I/O) |
<orc/support/vbi_types.h> |
VBI line data structures shared by the VBI decoder and observers |
<orc/support/vbi_utilities.h> |
VBI bit-extraction and manchester/biphase decode helpers |
Deprecated pre-tier include paths¶
The 27 flat <orc/plugin/...> / <orc/stage/...> paths that
predate this layout are retained as forwarding shims for one release, gated
by the ORC_SDK_DEPRECATED_INCLUDE_SHIMS CMake option (default ON). New
code must use the tiered paths above; building with the option OFF turns any
remaining pre-tier include into a hard compile error.
The manifest is also the source of the enforced include allowlist
(cmake/sdk_header_allowlist.txt, generated by
tools/gen_sdk_header_allowlist.sh): the gate script
(cmake/check_plugin_private_includes.sh) fails the build on any plugin
include outside it, other than plugin-local headers, standard-library and
platform headers, and permitted third-party headers (fmt/spdlog
unconditionally; other libraries when declared by the plugin's own CMake
target).
Design Notes on the Allowlist¶
A few allowlist entries carry rationale worth knowing:
- Logging surface. In-tree plugins use the
ORC_LOG_*macro family from the support-tier header<orc/support/logging.h>, backed by the host's spdlog logger. Out-of-tree plugins should prefer the services-table macros (ORC_PLUGIN_LOG_*from<orc/plugin/orc_plugin_services_helpers.h>) to avoid a direct spdlog dependency. The application logging headerorc/common/include/logging.h(get_app_logger()) is unrelated and is the GUI/CLI surface only; the contract header lives at<orc/support/logging.h>precisely so the two can never be confused via include-path order. - Observer classes were removed (ABI 10). The concrete
<orc/stage/observation/*_observer.h>classes and theObserverbase (<orc/stage/observation/observer.h>), deprecated in ABI 9, are removed from the SDK as of ABI 10: they are host-internal and no longer shipped inorc-sdk-support. Obtain observations through the host-ownedIObservationService, selecting observers by stable id. (The remaining<orc/stage/observation/...>entries —observation_context*.h,observation_schema.h, andobservation_service_interface.h— stay part of the contract.) lru_cache.his a self-contained generic container with no host coupling, allowlisted because several plugins legitimately use it for frame caching — preferable to each plugin vendoring a copy.
Stability Guarantees¶
Headers in the public SDK carry an explicit stability commitment:
- Changes to ABI-level contracts require a bump to kStagePluginHostAbiVersion.
- Changes to stage API contracts require a bump to kStagePluginApiVersion.
- Incompatible plugins are refused by the host with a diagnostic message;
they are never silently mis-loaded.
What Plugins Must Not Include¶
The SDK boundary is an allowlist: anything not listed in the SDK Headers section above (or covered by the plugin-local / standard-library / third-party allowances) is private and will change without notice. Typical private surfaces plugins must never reach into:
| Forbidden header path | Reason |
|---|---|
orc/core/include/*.h |
Internal host implementation headers (e.g. dag_executor.h, preview_renderer.h, stage_registry.h, buffered_file_io.h) |
orc/core/analysis/*.h, orc/core/observers/*.h |
Host analysis/observer internals (the observer headers plugins may use live under <orc/stage/observation/...>) |
orc/presenters/*.h |
Presenter layer — GUI/CLI internal |
orc/gui/*.h |
GUI internal |
orc/cli/*.h |
CLI internal |
The boundary is enforced twice:
- At compile time — the
orc::plugin-sdktarget exposes only the SDK include tree (plus spdlog/fmt) and theorc::orc-sdk-supportstatic library (support-tier helper symbols). It does not link the host, so a plugin translation unit that includes a private host header fails to compile. - By the allowlist scan —
check_plugin_private_includes.sh(a hard CI gate,ctest -L sdk) fails on any include outside the allowlist even if it would compile.
Do not link directly against orc-core, orc-presenters, orc-gui, or
orc-cli. Link only against orc::plugin-sdk. Third-party libraries a
plugin needs (FFmpeg, SQLite, FFTW, ...) must be declared by the plugin's
own CMake target — they are no longer inherited from the host.
If a capability you need is missing from the SDK, open an issue in the decode-orc repository. Missing capability is never a reason to include private headers or link private internals.
CMake Integration¶
Obtaining the SDK¶
The SDK is installed as part of a decode-orc install tree. From a decode-orc checkout:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
cmake --install build --prefix /path/to/sdk-prefix
The prefix then contains the CMake package
(lib/cmake/decode-orc-plugin-sdk/), the public SDK headers
(include/decode-orc-plugin-sdk/), the orc-sdk-support static library
(lib/liborc-sdk-support.a), and the orc-cli/orc-gui binaries used to load
and test your plugin. Plugins link the SDK alone — the host libraries
(orc-core, orc-common) are not part of the SDK package.
CI also publishes the SDK on its own as
decode-orc-plugin-sdk-<version>-<platform>.tar.gz (headers + static library +
CMake config + enforcement scripts). Unpack it anywhere and point
CMAKE_PREFIX_PATH at the unpacked directory — no decode-orc source tree or its
FFmpeg / yaml-cpp / PNG / SQLite3 dependency stack is required, only spdlog and
fmt. FetchContent_Declare(... URL <tarball>) works the same way.
Out-of-tree plugin project¶
cmake_minimum_required(VERSION 3.20)
project(my-orc-stage VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(decode-orc-plugin-sdk REQUIRED)
orc_add_stage_plugin(
my-orc-stage-my-filter
OUTPUT_NAME orc-plugin_my-filter_my-stage
PLUGIN_VERSION "${PROJECT_VERSION}"
SOURCES plugin.cpp my_filter_stage.cpp
)
Configure with the SDK prefix on CMAKE_PREFIX_PATH:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH=/path/to/sdk-prefix
cmake --build build -j
The built plugin lands in <build-dir>/lib/orc-stage-plugins/. Load it into
the host with the plugin search path environment variable:
The installed decode-orc-plugin-sdk CMake package provides:
- orc::plugin-sdk — imported INTERFACE target with the SDK include paths,
spdlog/fmt, and the orc::orc-sdk-support static library (support-tier
helper symbols). No host library is linked
- orc::orc-sdk-support — the support-tier helper static library, pulled in
transitively by orc::plugin-sdk
- orc_add_stage_plugin() — plugin target helper macro
- The public SDK headers (<orc/abi/...>, <orc/stage/...>, <orc/support/...>)
- check_plugin_private_includes.sh / check_plugin_private_links.sh — SDK enforcement gate scripts
The package's only find_dependency requirements are spdlog and fmt.
This exact flow is validated in decode-orc CI on every push: the fixture
plugin orc-tests/fixtures/external-stage-plugin/ is built against a
freshly installed prefix and loaded into the installed orc-cli.
Validating SDK-only compliance locally¶
To run the same enforcement gates that execute in Decode-Orc CI/CD, use the
scripts bundled with the installed SDK package (located at
<install-prefix>/lib/cmake/decode-orc-plugin-sdk/):
# Check every include in your plugin tree against the SDK allowlist
bash <sdk-install>/check_plugin_private_includes.sh /path/to/your-plugin-repo
# Check for forbidden private-link dependencies in your plugin target
bash <sdk-install>/check_plugin_private_links.sh /path/to/your-plugin-repo
When pointed at a tree that is not a decode-orc checkout, both scripts run in standalone mode: the whole tree is scanned as a single plugin (includes may resolve anywhere within it). Both scripts exit with code 1 and print a diagnostic on violations. Run them before opening a pull request or tagging a release.
Output naming¶
Follow the artifact naming convention so the host can validate and cache plugin binaries correctly:
Implementing a Stage¶
Minimal plugin structure¶
The registration helpers in <orc/abi/orc_plugin_registration.h> (pulled
in by the umbrella header) expand the entire plugin boilerplate — descriptor
version/toolchain fields, both entrypoints, service-table storage, and stage
registration — from two statements:
#include <orc/abi/orc_plugin_sdk.h>
#include "my_stage.h" // Your DAGStage subclass
namespace {
// The ABI version, API version, and toolchain tag are filled in from the SDK
// the plugin is compiled against. All pointer fields point to static storage.
constexpr orc::StagePluginDescriptor kDescriptor =
ORC_STAGE_PLUGIN_DESCRIPTOR("com.example.stage.my-filter", // plugin_id
"1.0.0", // plugin_version
"MIT", // license_spdx
false); // is_core_plugin
} // namespace
// Expands to both required entrypoints. MyStage is registered under the
// stage name reported by its own get_node_type_info().stage_name — the stage
// class is the single source of truth for registration metadata. List
// additional stage types as further arguments to register several stages
// from one plugin.
ORC_DEFINE_STAGE_PLUGIN(kDescriptor, MyStage)
Stage types passed to ORC_DEFINE_STAGE_PLUGIN must be either
default-constructible or constructible from orc::IStageServices*; in the
latter case the host's consolidated stage services are injected automatically
when the stage is created.
The same pattern is used by every bundled stage plugin — see
orc/plugins/stages/mask_line/plugin.cpp (single stage) or
orc/plugins/stages/cvbs_source/plugin.cpp (multiple stages) in the
Decode-Orc repository for complete real-world examples.
Raw entrypoints (custom registration logic)¶
Plugins that need custom registration logic — stages with other constructor signatures, conditional registration, dynamic stage sets — can write the two exported entrypoints by hand instead:
#include <orc/abi/orc_plugin_sdk.h>
#include "my_stage.h"
namespace {
// Factory the host calls to create stage instances on demand.
// Must match orc::OrcStageFactoryFn: std::shared_ptr<DAGStage> (*)().
orc::DAGStagePtr create_my_stage() { return std::make_shared<MyStage>(); }
// Plugin descriptor. Fields are positional and must follow the declaration
// order in <orc/abi/orc_plugin_abi.h>. All pointer fields must point to
// static storage.
constexpr orc::StagePluginDescriptor kDescriptor{
"com.example.stage.my-filter", // plugin_id
"1.0.0", // plugin_version
orc::kStagePluginHostAbiVersion, // host_abi_version
orc::kStagePluginApiVersion, // plugin_api_version
"MIT", // license_spdx
false, // is_core_plugin
ORC_SDK_TOOLCHAIN_TAG, // toolchain_tag
};
} // namespace
ORC_STAGE_PLUGIN_EXPORT const orc::StagePluginDescriptor*
orc_get_stage_plugin_descriptor() {
return &kDescriptor;
}
ORC_STAGE_PLUGIN_EXPORT bool orc_register_stage_plugin(
const orc::OrcPluginServices* services, void* context,
bool (*register_stage)(void* context, const char* stage_name,
orc::OrcStageFactoryFn factory),
const char** error_message) {
// Store the host service table before registering any stage; the pointer
// remains valid for the lifetime of the loaded plugin.
orc::plugin::set_services(services);
if (!register_stage) {
if (error_message) {
*error_message = "Missing stage registration callback";
}
return false;
}
if (!register_stage(context, "my_filter", &create_my_stage)) {
if (error_message) {
*error_message = "Host rejected stage registration";
}
return false;
}
return true;
}
Stage interfaces¶
Implement one or more of these interfaces depending on stage behaviour:
| Interface | Use when |
|---|---|
DAGStage |
Base interface — all stages |
ParameterizedStage |
Stage exposes user-configurable parameters |
TriggerableStage |
Stage responds to external trigger events |
Receiving data¶
Override DAGStage::execute() to receive ArtifactPtr inputs from upstream
nodes. Return a vector of ArtifactPtr outputs to pass downstream.
Transform stages: VideoFrameRepresentationWrapper contract¶
Transform stages that expose video output extend
VideoFrameRepresentationWrapper (in
<orc/stage/video_frame_representation.h>). The wrapper's public read API
always describes the stage's output: downstream stages, observers, and
analysis sinks can never reach unprocessed upstream data through any
accessor, and stages themselves may only read from the wrapped input
(source_), never from anything further upstream.
The accessors fall into two groups:
- Pass-through primitives (forwarded to the wrapped input): navigation
(
frame_range,frame_count,has_frame,get_frame_descriptor),get_frame,has_separate_channels,get_frame_luma,get_frame_chroma,get_dropout_hints,get_video_parameters, and the audio / EFM / AC3 accessors. A stage whose output differs from its input for one of these must override it. A stage that remaps frame IDs must override every per-frame accessor in this group so IDs are translated — for audio that meansget_audio_samples(per channel pair): the output frame at timeline index p must serve exactlyaudio_pairs_in_frame(p)pairs, truncating or silence-padding by one pair when the mapping breaks the NTSC/PAL-M cadence phase (see<orc/stage/audio/audio_channel_pair.h>). - Derived accessors (
get_line,get_line_samples,get_frame_copy,get_line_luma,get_line_chroma): implemented by the wrapper in terms of the object's own virtual primitives, never forwarded. Overriding the frame-level primitives is therefore sufficient for correctness on every read path; overriding a derived accessor is purely an optimisation (for example, a stage that does not modify sample data may forward line reads to the wrapped input to preserve a source's seek-one-line-from-disk fast path).
This contract is verified by
orc-tests/core/unit/contracts/video_frame_representation_wrapper_contract_test.cpp.
Parameters¶
ParameterizedStage exposes three methods:
get_parameter_descriptors(VideoSystem, SourceType)— returns the list ofParameterDescriptorrecords this stage supports, filtered by the project's video format and source type (a no-argument convenience overload passesUnknownfor both).get_parameters()— returns the current values as astd::map<std::string, ParameterValue>.set_parameters(const std::map<std::string, ParameterValue>&)— applies new values; returnstrueif all parameters were valid and set successfully.
ParameterValue is a std::variant holding one of: int32_t, uint32_t,
double, bool, or std::string. The corresponding ParameterType enum
additionally distinguishes FILE_PATH (a string for which the GUI shows a
file browser).
Configuration status¶
Stages can report their readiness to the host UI via set_configuration_status(),
which accepts a value from the orc::ConfigurationStatus enum:
| Value | Meaning |
|---|---|
ConfigurationStatus::Green |
Fully configured or requires no parameters |
ConfigurationStatus::Yellow |
Partially configured; some parameters are missing or empty |
ConfigurationStatus::Red |
Unconfigured; critical required parameters are absent |
The host renders a small coloured dot on the stage node in the pipeline graph to give the user at-a-glance feedback.
When to call set_configuration_status():
- In the constructor — set
Yellowif the stage requires at least one parameter that has no sensible default,Greenif all parameters have working defaults. - At the end of
set_parameters()— re-evaluate and call again based on the values that were just applied.
MyStage::MyStage() {
set_configuration_status(orc::ConfigurationStatus::Yellow);
}
bool MyStage::set_parameters(
const std::map<std::string, orc::ParameterValue>& params) {
auto it = params.find("output_path");
const bool ready =
it != params.end() &&
std::holds_alternative<std::string>(it->second) &&
!std::get<std::string>(it->second).empty();
set_configuration_status(ready ? orc::ConfigurationStatus::Green
: orc::ConfigurationStatus::Yellow);
return true;
}
Read the current value with get_configuration_status() if needed. The default
(when set_configuration_status() is never called) is Green.
Host services¶
The host passes an OrcPluginServices table as the first argument to
orc_register_stage_plugin(). Store it with orc::plugin::set_services()
before registering any stage. The table provides:
log— pre-formatted message logging routed to the host logger. Use theORC_PLUGIN_LOG_TRACE/DEBUG/INFO/WARN/ERROR/CRITICALmacros (from<orc/plugin/orc_plugin_services_helpers.h>) rather than calling the function pointer directly. Logging does not go throughIStageServices.render_colour_preview— converts a decodedColourFrameCarrierto a display-readyPreviewImagefor preview-capable stages.stage_services— optional pointer to the consolidatedIStageServicesinterface; may benullptrwhen the capability is unavailable. Retrieve it withorc::plugin::get_stage_services()(no arguments), which returnsnullptrif the services table is absent or predates the field.observation_service— optional pointer to theIObservationServiceinterface (added in ABI 9). Retrieve it withorc::plugin::get_observation_service()(no arguments), which returnsnullptrif the services table is absent or predates the field (any host on ABI 8 or earlier). See Observation service.
IStageServices currently exposes exactly three factory methods, used by sink
stages for buffered file output:
create_buffered_file_writer_uint8(size_t buffer_size)— returns astd::shared_ptr<IFileWriterUint8>create_buffered_file_writer_uint16(size_t buffer_size)— returns astd::shared_ptr<IFileWriterUint16>create_buffered_file_writer_int16(size_t buffer_size)— returns astd::shared_ptr<IFileWriterInt16>(16-bit signed PCM output, e.g. WAV audio)
New IStageServices methods are appended after existing entries (append-only
convention), so plugins built against an older SDK keep working with a newer
host within the same version pair.
Progress reporting and artifact-delivery callbacks are not part of the current services contract. If your stage needs a host capability that is missing, request an SDK extension rather than working around it.
Observation service (ABI 9)¶
IObservationService
(<orc/stage/observation/observation_service_interface.h>) is a host-owned
service that runs the standard observers. The observer implementations live
once, in the host; a plugin selects one by stable string id instead of
linking the concrete observer classes. This mirrors render_colour_preview:
the capability crosses the boundary as a host-owned interface, not as object
code compiled into every plugin.
Retrieve it with orc::plugin::get_observation_service(), which returns
nullptr on any host that predates ABI 9. A plugin must null-check the
result and degrade gracefully (skip the observation, log a warning) when the
service is absent.
The interface exposes:
available_observers()— returns oneObserverInfo { id, version, provided_observations }per observer the host offers, in a stable order.create_observer(observer_id)— returns an owningstd::unique_ptr<IObserverHandle>for a stateful session, ornullptrfor an unknown id. Use this when observations accumulate across frames (e.g. the closed-caption observer pairs successive fields). Drive it withhandle->process_frame(representation, frame_id, context).run_observer(observer_id, representation, frame_id, context)— a one-shot convenience wrapper that creates a throwaway handle, processes exactly one frame, and returnsfalsefor an unknown id (leavingcontextuntouched). Do not use it where cross-frame state matters.
No method throws across the plugin boundary: an unknown id yields a null handle
or false, never an exception. Thread-safety is documented on each interface
method — in short, the service is safe to call concurrently, but a single
IObserverHandle must be driven from one thread at a time.
Migrating from the observer classes¶
The concrete observer classes under <orc/stage/observation/*_observer.h>
(BiphaseObserver, WhiteSNRObserver, …) were deprecated in ABI 9 and are
removed in ABI 10: the headers have left the plugin-facing SDK and
orc-sdk-support no longer ships their object code. Replace direct construction
with a service call keyed by the id below:
| Removed class | Observer id | Former header (removed in ABI 10) |
|---|---|---|
BiphaseObserver |
biphase |
<orc/stage/observation/biphase_observer.h> |
BlackPSNRObserver |
black_psnr |
<orc/stage/observation/black_psnr_observer.h> |
BurstLevelObserver |
burst_level |
<orc/stage/observation/burst_level_observer.h> |
ClosedCaptionObserver |
closed_caption |
<orc/stage/observation/closed_caption_observer.h> |
ColourFramePhaseObserver |
colour_frame_phase |
<orc/stage/observation/colour_frame_phase_observer.h> |
FieldQualityObserver |
disc_quality |
<orc/stage/observation/field_quality_observer.h> |
FmCodeObserver |
fm_code |
<orc/stage/observation/fm_code_observer.h> |
WhiteFlagObserver |
white_flag |
<orc/stage/observation/white_flag_observer.h> |
WhiteSNRObserver |
white_snr |
<orc/stage/observation/white_snr_observer.h> |
Before (linked observer class):
#include <orc/stage/observation/closed_caption_observer.h>
orc::ClosedCaptionObserver observer_; // persistent member, stateful
observer_.process_frame(representation, frame_id, context);
After (host service, id-selected):
#include <orc/stage/observation/observation_service_interface.h>
// Once, when the stage is constructed:
orc::IObservationService* observation_service =
orc::plugin::get_observation_service();
if (observation_service) {
observer_ = observation_service->create_observer("closed_caption");
}
// Per frame (observer_ is a std::unique_ptr<orc::IObserverHandle> member):
if (observer_) {
observer_->process_frame(representation, frame_id, context);
}
For a stateless one-off, skip the handle and call
observation_service->run_observer("closed_caption", representation, frame_id,
context).
Optional: Stage tools¶
If your stage provides an interactive tool (e.g., a custom editor or analysis
view), implement StageToolProvider and return a vector of StageToolDescriptor
from get_stage_tools(). The host discovers these descriptors through the
presenter layer and routes them to the GUI without any hardcoded stage-name
dispatch.
Testing¶
Unit-test your stage implementation in isolation. Inject a mock IStageServices
rather than depending on a live host. The skeleton template includes example
Google Test suites and CMake test registration.
Recommended ctest labels for plugin test suites:
| Label | Scope |
|---|---|
unit |
All fast, isolated stage unit tests |
sources |
Source stage tests |
transforms |
Transform stage tests |
sinks |
Sink stage tests |
Distribution¶
GitHub release assets¶
Publish platform binaries as GitHub release assets using the naming convention:
where <platform> is linux/macos/windows, <ext> is
so/dylib/dll, and the optional _abi<N> token records the host ABI the
binary was built against (e.g. orc-plugin_my-stage_linux_abi8.so). Examples:
orc-plugin_my-stage_linux.so # legacy, untagged
orc-plugin_my-stage_linux_abi8.so # ABI-tagged (recommended)
orc-plugin_my-stage_macos_abi8.dylib
orc-plugin_my-stage_windows_abi8.dll
Tagging lets a single release ship builds for several host ABI versions side
by side. When resolving a release the host prefers the asset tagged for its
own ABI (_abi<host>), then falls back to a legacy untagged name (validated
at load time), and only selects an asset tagged for a different ABI as a last
resort — reporting "needs a rebuild" before it ever downloads a build it
cannot load. Untagged names remain valid for backward compatibility.
The skeleton CI workflows produce and upload these artifacts automatically on tagged releases.
Registry entry¶
Users register your plugin by adding an entry to their plugin registry YAML:
- plugin_id: com.example.my-stage
plugin_version: "1.0.0"
source_repo_url: https://github.com/example-org/orc-plugin_my-stage
artifact_source: github_release_asset
release_asset_url: https://github.com/example-org/orc-plugin_my-stage/releases/download/v1.0.0/orc-plugin_my-stage_linux.so
release_tag: v1.0.0
release_asset_name: orc-plugin_my-stage_linux.so # platform-specific
target_platform: linux
required_host_abi: 8
enabled: true
trust_state: untrusted
license_spdx: MIT
sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
The host downloads and caches the binary automatically the first time it
starts with this entry present — but only once the entry is trusted;
entries with trust_state: untrusted are neither downloaded nor loaded.
Trust is a decision separate from adding or enabling: adding a plugin through
the GUI Plugin Manager (local file, URL, or from the curated index) prompts an
explicit trust-confirmation dialog, and toggling Enabled never grants
trust. Entries supplied any other way — such as the hand-written snippet above
— default to untrusted and must be trusted via the Plugin Manager's Trusted
column or orc-cli plugins trust <id> (the CLI plugins add --trusted flag
trusts at add time). Publish the artifact's SHA-256 digest
so users can record it in the optional sha256 field: the host then
verifies the download (and every
cache hit) against it and quarantines mismatching files. Without a sha256
the plugin still loads, with a warning that its integrity was not verified.
Plugin binaries are not code-signed; see the "Distribution integrity"
section of
plugin-architecture.md for
exactly what is and is not verified.
Curated index and discovery¶
To reach users without hand-written YAML, list your plugin in the curated
index (orc-plugin-registry/). Open a
pull request adding an entry with per-(platform, host ABI) artifacts, each
carrying a mandatory sha256; a maintainer's merge publishes it immediately.
Users then discover and install it without knowing your URL:
$ orc-cli plugins search deinterlace # find listed plugins
$ orc-cli plugins info com.example.my-stage
$ orc-cli plugins install com.example.my-stage # recorded untrusted
$ orc-cli plugins trust com.example.my-stage # confirm trust
The GUI exposes the same flow through Plugin Manager → Browse Plugins….
Installing from the index records the entry with the index-declared sha256
and leaves it untrusted until the user confirms trust. Hosts resolve the
artifact matching their platform and ABI, so a user on an unsupported host is
told "no build for this host" instead of downloading an incompatible binary.
Versioning and Compatibility Policy¶
Binary compatibility model¶
The plugin boundary is a C++ ABI, not a C ABI. std::shared_ptr,
std::string, STL containers, exceptions, and virtual-function tables all
cross the host/plugin boundary. Matching version numbers are therefore
necessary but not sufficient: a plugin must be built with the same compiler
family, the same C++ standard library, and a compatible build configuration
as the host. On Windows this includes the CRT flavour — a Debug-CRT plugin
cannot be loaded by a Release-CRT host.
Since ABI v5 this requirement is enforced at load time: the descriptor's
toolchain_tag field (populated by the ORC_SDK_TOOLCHAIN_TAG macro, and
automatically by ORC_STAGE_PLUGIN_DESCRIPTOR) encodes the compiler family
and major version, the C++ standard library, and — on Windows — the CRT
flavour, e.g. gcc14/libstdc++, clang17/libc++,
msvc19/msvc-stl/release-crt. The host requires the plugin's tag to equal
its own tag exactly and rejects the plugin with a diagnostic naming both
tags otherwise. The policy is deliberately conservative, consistent with the
exact-match rule for the version numbers.
The host requires exact equality for both host_abi_version and
plugin_api_version; a mismatch in either causes the plugin to be rejected
with a logged diagnostic. The services_size field in OrcPluginServices
is an intra-version safety net only: it guards access to service-table
fields appended within the current ABI version. It is not a cross-version
compatibility mechanism.
Version history¶
The table below is generated from orc/sdk/abi_history.yaml — the single
source of truth for the ABI/API version log. Do not edit it by hand; run
tools/gen_abi_history_docs.sh and splice the output between the markers. The
AbiVersionDocsSync CTest (label sdk) fails when this block is stale.
host_abi_version |
plugin_api_version |
Change |
|---|---|---|
| 1 | — | Initial internal release; plugin_api_version not yet in descriptor |
| 2 | 1 | plugin_api_version added to StagePluginDescriptor; public SDK headers published |
| 3 | 1 | OrcPluginServices table added; orc_register_stage_plugin now receives const OrcPluginServices* as its first parameter; plugins must use the services table for logging instead of resolving host symbols directly |
| 4 | 2 | Decode-Orc 2.0: VideoFrameRepresentation replaces VideoFieldRepresentation as the primary frame-data contract; DAGStage::execute() operates on frame-based artifacts; DropoutRun replaces DropoutRegion; FieldID/FieldIDRange removed — use FrameID/FrameIDRange. All plugins must be rebuilt against the v2.0 SDK |
| 5 | 2 | StagePluginDescriptor gains the appended toolchain_tag field (populate with ORC_SDK_TOOLCHAIN_TAG); the loader requires the plugin's tag to equal the host's exactly. Registration helpers (ORC_STAGE_PLUGIN_DESCRIPTOR, ORC_DEFINE_STAGE_PLUGIN) added in <orc/abi/orc_plugin_registration.h> |
| 6 | 2 | Multi-track audio: VideoFrameRepresentation's single-track audio accessors (audio_locked(), get_audio_sample_count(FrameID), get_audio_samples(FrameID)) are replaced by a track-indexed API — audio_track_count(), get_audio_track_descriptor(track), per-track frame-locked accessors, and per-track free-running stream accessors (get_audio_stream_pair_count / get_audio_stream_samples). New contract header <orc/stage/audio_track.h>. The vtable layout change requires all plugins to be rebuilt |
| 7 | 2 | Channel-pair audio (SMPTE 272M-1994): the track-indexed audio API is replaced by the channel-pair API — audio_channel_pair_count(), get_audio_channel_pair_descriptor(pair), and get_audio_samples(pair, id) returning 24-bit-in-int32 samples. All audio is 48 kHz frame-locked (synchronous); the free-running stream accessors are removed. Contract header <orc/stage/audio_track.h> is replaced by <orc/stage/audio/audio_channel_pair.h>. The vtable layout change requires all plugins to be rebuilt |
| 8 | 2 | VideoFrameRepresentation gains prime_audio_decode(): a hook that forces a deferred whole-stream audio decode (e.g. EFM audio) to run up front with progress reporting, forwarded down the wrapper chain so sinks can meter it on the progress dialog. The appended virtual changes the vtable layout, requiring all plugins to be rebuilt |
| 9 | 2 | OrcPluginServices gains the appended observation_service pointer (IObservationService, new contract header <orc/stage/observation/observation_service_interface.h>): a host-owned service that runs the standard observers by stable string id, reached via plugin::get_observation_service(). Guarded by services_size; older hosts leave it null. Appended field only — plugins need not be rebuilt to keep working against ABI 8 behaviour |
| 10 | 2 | The concrete observer classes (the nine <orc/stage/observation/*_observer.h> headers — BiphaseObserver, WhiteSNRObserver, …) and the Observer base (<orc/stage/observation/observer.h>) are removed from the plugin SDK: observers are now host-internal and reached exclusively through the IObservationService added in ABI 9, selected by stable string id. orc-sdk-support no longer ships observer object code, and the deprecated pre-tier observation include-path shims (<orc/stage/observers/...> and the flat <orc/stage/observation_*.h> paths) are removed. observation_schema.h, observation_context*.h, and observation_service_interface.h remain the contract. Source-breaking for any plugin still including the observer classes — migrate to IObservationService::create_observer(id) |
ABI impact decision table¶
Use this table to decide whether a change forces a host_abi_version bump.
When in doubt, treat the change as ABI-breaking. The bump-procedure guard
(tools/check_abi_bump.sh, run in CI) references this table in its failure
message.
| Change | Bumps the host ABI? |
|---|---|
Reorder, resize, or retype a StagePluginDescriptor field |
Yes |
Change an entrypoint signature or the register_stage callback contract |
Yes |
Add, remove, or reorder a virtual on a contract type crossing the boundary (e.g. VideoFrameRepresentation, DAGStage, an observer interface) |
Yes |
| Change the layout of any type passed across the boundary by value or by reference | Yes |
| Change the meaning/encoding of an existing cross-boundary type without a layout change | Yes (semantic break) |
Append a field to OrcPluginServices guarded by services_size |
No |
| Append a new capability interface reached via an accessor (see below) | No |
Change a support-tier header (orc/support/…: lru_cache.h, logging.h, helpers) |
No — recompile at leisure |
Change registration-template internals (ORC_DEFINE_STAGE_PLUGIN) without altering the descriptor layout |
No |
Edit comments, docs, or instructions.md |
No |
Preferred additive patterns¶
Two mechanisms let the host and the stage contract grow without a bump — prefer them over editing an existing layout:
services_sizeappend-only growth. New host services are appended toOrcPluginServices; a plugin checksservices->services_sizebefore reading a field its compiled ABI does not guarantee. This is the sanctioned way to add host capabilities.- Capability-accessor factoring. To grow a churn-prone domain (audio has
driven five of the last ABI bumps), add a new interface reached through an
accessor rather than new virtuals on the one vtable every plugin depends
on — e.g.
frame.audio()returning anIFrameAudio*(headerorc/stage/audio/). Adding a brand-new interface is additive and changes no existing vtable layout, whereas appending a virtual toVideoFrameRepresentationinvalidates every plugin. Apply this opportunistically at the next plannedVideoFrameRepresentationrevision rather than as a standalone bump.
See also the compatibility-gating section of plugin-architecture.md.
When the host increments a version¶
If host_abi_version increases, you must rebuild your plugin against the new
SDK. The host will refuse to load binaries built against an older ABI.
If plugin_api_version increases, your plugin must update its stage
implementation to match the new stage contract and recompile.
Neither increment is made without a corresponding Decode-Orc release and migration notes.
Repository Naming Convention¶
External plugin repository names should use the prefix orc-plugin_:
This convention applies to repositories hosted under the decode-orc organisation and is recommended for all third-party plugin authors.