Sample CodemacOSReviewed 2026-11-01View on Apple Developer

Supporting custom media formats and decoders

At a glance

Item Summary
Purpose Extend the media formats the system can open. A Mac app embeds two MediaExtension app extensions: a format reader for the custom “Simple Container Format” (.simpl) and a video decoder for a run-length codec (XRLE). The app plays any movie and exports movies in either format.
App architecture Three targets. A SwiftUI + Objective-C host app (FormatReaderAndDecoder) and two ExtensionKit extensions (SimpleContainerReader, RLEVideoDecoder) embedded in the app. Each extension’s EXPrincipalClass is a factory that conforms to a MediaExtension protocol and creates per-file or per-stream worker objects. The extensions never call each other or the app. They share only C header contracts in Shared/.
Main patterns Factory (principal-class factories, plus subclass selection by media type), Delegate or data-source callbacks (RLEMovieExporterDelegate, DropDelegate), Binding-based state propagation (save-panel modifier)
Project style One folder per target, plus Shared/ for headers used across targets. The host app keeps its Objective-C export pipeline in RLEMovieExporter/ and exposes it to Swift through a bridging header.
Execution model App: Swift 5 mode with no default actor isolation. It uses Task.detached for blocking registration, a plain Task for export, and Task { @MainActor in } hops for UI state. The exporter uses a serial dispatch queue, a dispatch group, semaphores, and a C11 atomic cancel flag. Extensions: synchronous completion handlers, with no queues or locks of their own.
State/event model @Observable MovieModel held in @State. A weak Objective-C delegate reports progress. MediaExtension protocols use completion-handler callbacks. The decoder posts a readiness notification.
Key frameworks/packages Swift modules SwiftUI, AVKit, VideoToolbox, UniformTypeIdentifiers. System headers <MediaExtension/MediaExtension.h>, <CoreMedia/CoreMedia.h>, <VideoToolbox/VideoToolbox.h>, <CoreVideo/CoreVideo.h>. Quoted local headers "SimpleContainerFormat.h", "RLECodec.h". No Swift packages.

Project structure

FormatReaderAndDecoder.xcodeproj   # 3 targets: app + 2 extensionkit-extension
FormatReaderAndDecoder/            # host app (Swift UI + ObjC exporter)
├── FormatReaderAndDecoderApp.swift
├── ContentView.swift              # VideoPlayer, drop target, export UI
├── MovieModel.swift               # @Observable owner; registration Task
├── MovieSavePanel.swift           # NSSavePanel ViewModifier
├── Info.plist                     # exports the .simpl UTType
└── RLEMovieExporter/              # ObjC: AVFoundation read -> RLE -> write
    ├── RLEMovieExporter.{h,m}     # pipeline + RLEMovieExporterDelegate
    ├── AssetVideoReader / AssetAudioReader
    ├── RLECompressor              # 2vuy pixels -> XRLE bytes
    ├── AssetSampleWriter          # QuickTime via AVAssetWriter
    └── SimpleContainerWriter      # .simpl file writer
SimpleContainerReader/             # format reader extension
├── Info.plist                     # EXPrincipalClass = ExtensionFormatReaderFactory
├── ExtensionFormatReader.{h,m}    # factory + MEFormatReader
├── ExtensionTrackReader.{h,m}     # METrackReader (+ video/audio/timecode)
├── ExtensionSampleCursor.{h,m}    # MESampleCursor (+ video/audio/timecode)
└── SimpleContainerReader.{h,m}    # container parser over MEByteSource
RLEVideoDecoder/                   # video decoder extension
├── Info.plist                     # EXPrincipalClass = ExtensionRLEDecoderFactory
├── ExtensionRLEDecoder.{h,m}      # factory + MEVideoDecoder
└── RLEDecompressor.{h,m}          # XRLE bytes -> 2vuy pixel buffer
Shared/
├── SimpleContainerFormat.h        # on-disk layout (writer <-> reader)
└── RLECodec.h                     # kRLECodecCode 'XRLE' (exporter <-> decoder)

Structure observations

  • Target types: the app is com.apple.product-type.application (FormatReaderAndDecoder.xcodeproj/project.pbxproj:310). Both extensions are com.apple.product-type.extensionkit-extension (FormatReaderAndDecoder.xcodeproj/project.pbxproj:327, FormatReaderAndDecoder.xcodeproj/project.pbxproj:344).
  • Embedding: the app depends on both extension targets (FormatReaderAndDecoder.xcodeproj/project.pbxproj:304, FormatReaderAndDecoder.xcodeproj/project.pbxproj:305). An “Embed ExtensionKit Extensions” copy phase places both .appex bundles in $(EXTENSIONS_FOLDER_PATH) (FormatReaderAndDecoder.xcodeproj/project.pbxproj:56).
  • Source membership is strictly per target. The app compiles only the four Swift files and the six RLEMovieExporter/*.m files. The reader compiles only its four .m files. The decoder compiles ExtensionRLEDecoder.m and RLEDecompressor.m. No implementation file is shared.
  • Shared/ contains headers only. SimpleContainerFormat.h is imported by the app’s writer (FormatReaderAndDecoder/RLEMovieExporter/SimpleContainerWriter.h:13) and the reader’s parser (SimpleContainerReader/SimpleContainerReader.h:13). RLECodec.h is imported by the exporter (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:9) and the decoder (RLEVideoDecoder/ExtensionRLEDecoder.m:10). These two headers are the only compile-time coupling between targets.
  • The app target is Swift 5 (FormatReaderAndDecoder.xcodeproj/project.pbxproj:503), deploys to macOS 15.0 (FormatReaderAndDecoder.xcodeproj/project.pbxproj:494), and bridges Objective-C through FormatReaderAndDecoder-Bridging-Header.h (FormatReaderAndDecoder.xcodeproj/project.pbxproj:501). That header imports only RLEMovieExporter.h (FormatReaderAndDecoder/RLEMovieExporter/FormatReaderAndDecoder-Bridging-Header.h:8).

Overall architecture

Reference code

SimpleContainerReader/Info.plist:5 — the reader’s extension-point binding

<key>EXAppExtensionAttributes</key>
<dict>
    <key>ClassImplementationID</key>
    <string>com.example.apple-samplecode.formatreader.SimpleContainer</string>
    <key>EXExtensionPointIdentifier</key>
    <string>com.apple.mediaextension.formatreader</string>
    <key>EXPrincipalClass</key>
    <string>ExtensionFormatReaderFactory</string>
    <key>MTFileNameExtensionArray</key>
    <array>
        <string>simpl</string>
    </array>
    <key>MTUTTypeArray</key>
    <array>
        <string>com.example.apple-samplecode.simpleformat</string>
    </array>
    <!-- ... -->
</dict>

RLEVideoDecoder/Info.plist:9 — the decoder advertises the codec four-character code it handles

<key>CodecInfo</key>
<array>
    <dict>
        <key>CodecName</key>
        <string>MediaExtension RLE Video Codec</string>
        <key>CodecType</key>
        <string>XRLE</string>
    </dict>
</array>
<key>EXExtensionPointIdentifier</key>
<string>com.apple.mediaextension.videodecoder</string>
<key>EXPrincipalClass</key>
<string>ExtensionRLEDecoderFactory</string>

Interpretation

Observed: no target calls into another. The app never imports an extension header. The reader never references the decoder. The decoder knows only the kRLECodecCode constant. Each extension states what it handles declaratively: the reader by UTType and file extension, the decoder by CodecType. Each also names its principal class and carries an entitlement for its extension point (SimpleContainerReader/SimpleContainerReader.entitlements:5, RLEVideoDecoder/RLEVideoDecoder.entitlements:5).

Inference: the system media stack is the only broker between the two extensions. MediaExtension APIs do this routing: they choose the reader from the file’s type and the decoder from the format description’s codec type. Nothing in the sample’s source makes that selection. This is a plug-in architecture with the framework as host. The sample’s own composition is therefore three independently loadable products connected only by shared data contracts (file layout and codec code), not by calls.

Ownership and state

Ownership evidence

FormatReaderAndDecoder/MovieModel.swift:13 — app-side state owner

@Observable class MovieModel: NSObject {
    // ...
    var exporter: RLEMovieExporter?
    // ...
    var inputURL: URL?
    // ...
    let player = AVPlayer()
    // ...
    var progressValue = 0.0
    // ...
    private let registration = Task.detached {
        VTRegisterProfessionalVideoWorkflowVideoDecoders()
        MTRegisterProfessionalVideoWorkflowFormatReaders()
    }
    // ...
}

SimpleContainerReader/ExtensionFormatReader.m:35 — one format reader per byte source parses eagerly and shares one container parser with its tracks

- (instancetype)initWithByteSource:(MEByteSource *)byteSource
                           options:(MEFormatReaderInstantiationOptions *)options
                             error:(NSError **)error {
    // ...
    self = [super init];
    if (self) {
        // Set up the reader.
        m_byteSource = byteSource;
        m_reader = [[SimpleContainerReader alloc] initWithByteSource:m_byteSource];
        // ...
        if (err == noErr) {
            // ...
            [self setupTracks];
            [self setupFileInfo];
            [self setupMetadata];
        }
    }
    // ...
    return self;
}
Owner Object or state Relationship Mutation authority
ContentView MovieModel, isChoosingExportDestination @State private (FormatReaderAndDecoder/ContentView.swift:13, FormatReaderAndDecoder/ContentView.swift:14) The view. The save-panel modifier resets the flag through a binding
MovieModel exporter Creates a fresh RLEMovieExporter for each export and sets itself as delegate (FormatReaderAndDecoder/MovieModel.swift:46, FormatReaderAndDecoder/MovieModel.swift:47). Releases it when progress reaches 1 (FormatReaderAndDecoder/MovieModel.swift:107) MovieModel
MovieModel inputURL, player current item Set after a drop, on the main actor (FormatReaderAndDecoder/MovieModel.swift:84, FormatReaderAndDecoder/MovieModel.swift:85) MovieModel
MovieModel registration task private let. Created at initialization, never cancelled, awaited before each load (FormatReaderAndDecoder/MovieModel.swift:32, FormatReaderAndDecoder/MovieModel.swift:83) —
RLEMovieExporter delegate weak (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.h:42), which avoids a retain cycle with MovieModel MovieModel
RLEMovieExporter Readers, compressor, writers, vidFmtDesc Created in startWriting:, released (and CFReleased) in finishWriting, which dealloc also calls (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:37, FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:467) RLEMovieExporter
ExtensionFormatReader m_reader, m_tracks, m_fileInfo, m_metadata Built once in the initializer. The load… methods return them unchanged (SimpleContainerReader/ExtensionFormatReader.m:79) Initializer only
ExtensionTrackReader / ExtensionSampleCursor SimpleContainerReader A strong reference to the same parser instance (SimpleContainerReader/ExtensionTrackReader.m:26, SimpleContainerReader/ExtensionSampleCursor.m:22). Each cursor owns its m_pts and a retained format description, released in dealloc (SimpleContainerReader/ExtensionSampleCursor.m:30) The cursor mutates its own m_pts when it steps (SimpleContainerReader/ExtensionSampleCursor.m:178)
ExtensionRLEDecoder m_decoder, m_pixelBufferManager, m_formatDescription Creates RLEDecompressor. Stores the system-supplied pixel-buffer manager. Stores the format-description pointer without CFRetain (RLEVideoDecoder/ExtensionRLEDecoder.m:51, RLEVideoDecoder/ExtensionRLEDecoder.m:52, RLEVideoDecoder/ExtensionRLEDecoder.m:62) Initializer only

Who creates the factories is not visible in the source. The principal classes are named in each Info.plist and have only a plain init (SimpleContainerReader/ExtensionFormatReader.m:17, RLEVideoDecoder/ExtensionRLEDecoder.m:18). Inference: the extension host instantiates them.

Concurrency, scheduling, and thread safety

Evidence limit: actor isolation, async/await, or Task creation does not by itself prove background-thread execution. A Sendable conformance alone does not prove thread-safe mutation.

The app target is Swift 5 (FormatReaderAndDecoder.xcodeproj/project.pbxproj:503) and sets no default actor isolation. MovieModel carries no actor annotation, so its methods are nonisolated. Main-actor placement happens only where the source writes @MainActor.

Concern Source mechanism Verified placement or handoff Evidence
Blocking setup off the caller Task.detached stored in private let registration Runs the two blocking registration calls. The comment says these cross XPC and run off the main thread. MovieModel retains the task for its lifetime and never cancels it. FormatReaderAndDecoder/MovieModel.swift:32
Dependency gate await self.registration.value Each dropped movie waits for registration before loading FormatReaderAndDecoder/MovieModel.swift:83
Main-actor hop from a framework callback Task { @MainActor in ... } inside loadFileRepresentation’s completion handler Sets inputURL and replaces the player item on the main actor. The queue that runs the completion handler is not shown in the source. FormatReaderAndDecoder/MovieModel.swift:78, FormatReaderAndDecoder/MovieModel.swift:80
Main-actor hop from a delegate callback Task { @MainActor in ... } in progressChanged(_:) Writes progressValue and clears exporter on the main actor. The comment says the exporter calls in “from its own background queue”. FormatReaderAndDecoder/MovieModel.swift:100
Deferred modal presentation Task { @MainActor in ... } in onChange Defers NSSavePanel.runModal() until after the SwiftUI transaction (stated in the comment), then calls onCompletion FormatReaderAndDecoder/MovieSavePanel.swift:30
Export work Task(priority: .medium) { ... } in nonisolated exportMovie(to:as:) The closure has no actor to inherit. The comment states the intent “Do the writing on a background thread”. The synchronous writeRLE… call runs inside this task for the whole export. No handle is kept, so nothing can cancel the task itself. FormatReaderAndDecoder/MovieModel.swift:52
Cancellation C11 atomic_int stopWriting, set by cancelWriting Polled by the QuickTime video and audio blocks and by the Simple Container loop FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.h:39, FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:463, FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:158, FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:344
Pull-based writing (QuickTime path) requestMediaDataWhenReadyOnQueue: on one serial dispatch_queue_t. A dispatch_group brackets both tracks Video and audio blocks run serially on the same queue. The calling thread blocks in dispatch_group_wait until both tracks leave the group. FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:87, FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:94, FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:212
Shared mutable block state __block err, curFrame, vFinished, aFinished, vidPTS, audPTS Mutated only inside blocks that the same serial queue runs. The outer function reads err only after dispatch_group_wait. FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:49
Reader-side mutual exclusion dispatch_semaphore_create(1) around copySampleBufferForTime: and dealloc Used as a mutex over the asset reader’s buffer state FormatReaderAndDecoder/RLEMovieExporter/AssetVideoReader.m:59, FormatReaderAndDecoder/RLEMovieExporter/AssetVideoReader.m:218, FormatReaderAndDecoder/RLEMovieExporter/AssetAudioReader.m:244
Async-to-sync adapters dispatch_semaphore_create(0) Waits up to 5 s for loadTracksWithMediaType:completionHandler:. AssetSampleWriter.dealloc waits until finishWritingWithCompletionHandler: signals. FormatReaderAndDecoder/RLEMovieExporter/AssetVideoReader.m:46, FormatReaderAndDecoder/RLEMovieExporter/AssetSampleWriter.m:43, FormatReaderAndDecoder/RLEMovieExporter/AssetSampleWriter.m:139
Extension callbacks MediaExtension completion handlers The reader’s load…/generate… and the decoder’s decodeFrameFromSampleBuffer: invoke the completion handler synchronously before returning. Neither extension creates a queue, task, or lock. The thread the system uses is not established. SimpleContainerReader/ExtensionFormatReader.m:80, SimpleContainerReader/ExtensionTrackReader.m:114, RLEVideoDecoder/ExtensionRLEDecoder.m:108
Decoder back-pressure signal NSNotificationCenter post of MEVideoDecoderReadyForMoreMediaDataDidChangeNotification Posted after every decode. isReadyForMoreMediaData always returns YES. RLEVideoDecoder/ExtensionRLEDecoder.m:115, RLEVideoDecoder/ExtensionRLEDecoder.m:236
Semaphore inside a cursor dispatch_semaphore waiting on stepInPresentationOrderByCount: Waits on the cursor’s own completion handler. That handler is already synchronous in this implementation. SimpleContainerReader/ExtensionSampleCursor.m:104, SimpleContainerReader/ExtensionSampleCursor.m:126

Reference code

FormatReaderAndDecoder/MovieModel.swift:97 — the delegate callback arrives off the main actor and hops before touching observable state

func progressChanged(_ progress: Double) {
    // The exporter calls this from its own background queue, so update the observable
    // properties that the content view reads on the main actor.
    Task { @MainActor in
        // The content view observes `progressValue`, so the progress appears in the window.
        self.progressValue = progress

        // Releasing the exporter when the export finishes tells the content view to
        // hide the export controls overlay.
        if self.progressValue == 1 {
            self.exporter = nil
        }
    }
}

FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:86 — serial queue + group for the QuickTime path (abbreviated)

if (err == 0) {
    decodingQueue = dispatch_queue_create("com.example.apple-samplecode.FormatReaderAndDecoder.export.decode", DISPATCH_QUEUE_SERIAL);
    decodingTrackGroup = dispatch_group_create();

    __weak id<RLEMovieExporterDelegate> delegate = _delegate;

    // Handle the video track.
    dispatch_group_enter(decodingTrackGroup);
    [qtWriter videoRequestMediaDataWhenReadyOnQueue:decodingQueue
                                         usingBlock:^{
                                             // ...
                                         }];
}

Observations and inferences:

  • Observed: the comment at FormatReaderAndDecoder/MovieModel.swift:98 says progress arrives on “its own background queue”. In the QuickTime path, intermediate progress does come from the serial decodingQueue block (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:153). In the Simple Container path, the loop runs on whatever thread runs the Swift export task (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:341). Both paths send the final progressChanged:1.0 from that calling thread (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:228, FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:364). The @MainActor hop makes all of these safe for the UI.
  • Inference: MovieModel is not actor-isolated, so the compiler does not enforce main-actor access to exporter. The export task reads exporter (FormatReaderAndDecoder/MovieModel.swift:54) at roughly the same time the main actor assigns it. The design relies on ordering, not on isolation.
  • Inference: the export task blocks a Swift concurrency thread for the whole export, because writeRLE… is synchronous and itself waits on a dispatch group. The comment calls this “a background thread”. A dedicated queue would avoid tying up a cooperative-pool thread.

State propagation, frameworks, and dependencies

Evidence limit: an import proves a source-level compilation dependency at the cited line. It does not prove runtime use, architectural adoption, or whether a Swift package is a direct application dependency.

Category Mechanism or module Verified role Evidence
State propagation @Observable + @State ContentView reads inputURL, exporter, progressValue, and suggestedExportName to drive opacity, overlays, the title, and the disabled state FormatReaderAndDecoder/MovieModel.swift:13, FormatReaderAndDecoder/ContentView.swift:26, FormatReaderAndDecoder/ContentView.swift:70
State propagation @Binding MovieSavePanelModifier observes isPresented and resets it to false FormatReaderAndDecoder/MovieSavePanel.swift:16, FormatReaderAndDecoder/MovieSavePanel.swift:26
Callback DropDelegate MovieModel.performDrop(info:) is the drop target handed to onDrop(of:delegate:) FormatReaderAndDecoder/ContentView.swift:25, FormatReaderAndDecoder/MovieModel.swift:67
Callback RLEMovieExporterDelegate (local Objective-C protocol) Progress from exporter to MovieModel FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.h:22
Swift module import SwiftUI, AVKit App UI and VideoPlayer FormatReaderAndDecoder/ContentView.swift:9, FormatReaderAndDecoder/ContentView.swift:10
Swift module import VideoToolbox, UniformTypeIdentifiers Registration calls and UTType FormatReaderAndDecoder/MovieModel.swift:9, FormatReaderAndDecoder/MovieModel.swift:10
Local header (bridged) "RLEMovieExporter.h" The only Objective-C surface exposed to Swift FormatReaderAndDecoder/RLEMovieExporter/FormatReaderAndDecoder-Bridging-Header.h:8
System header <MediaExtension/MediaExtension.h> Reader and decoder protocol declarations SimpleContainerReader/ExtensionFormatReader.h:11, RLEVideoDecoder/ExtensionRLEDecoder.h:13
System header <CoreMedia/CoreMedia.h>, <VideoToolbox/VideoToolbox.h>, <CoreVideo/CoreVideo.h> Time, format descriptions, sample and pixel buffers RLEVideoDecoder/ExtensionRLEDecoder.h:11, RLEVideoDecoder/ExtensionRLEDecoder.h:12, RLEVideoDecoder/RLEDecompressor.h:12
Local shared header "SimpleContainerFormat.h", "RLECodec.h" Cross-target data contracts (see Structure observations) Shared/SimpleContainerFormat.h:91, Shared/RLECodec.h:13
Linked frameworks MediaExtension.framework (both extensions), VideoToolbox.framework (decoder) Link-phase entries FormatReaderAndDecoder.xcodeproj/project.pbxproj:15, FormatReaderAndDecoder.xcodeproj/project.pbxproj:14, FormatReaderAndDecoder.xcodeproj/project.pbxproj:19
Type declaration UTExportedTypeDeclarations conforming to com.apple.mediaextension-content Declared in both the app and the reader Info.plist FormatReaderAndDecoder/Info.plist:10, SimpleContainerReader/Info.plist:29

The project declares no Swift package. The app target’s link phase lists no framework. A MediaToolbox.framework file reference exists in the project but is not in any link phase.

Class and protocol design

Reference code

SimpleContainerReader/ExtensionFormatReader.h:13 — principal class and per-file reader conformances

@interface ExtensionFormatReaderFactory : NSObject <MEFormatReaderExtension>

- (instancetype)init;
- (id<MEFormatReader>)formatReaderWithByteSource:(MEByteSource *)primaryByteSource
                                         options:(MEFormatReaderInstantiationOptions *)options
                                           error:(NSError **)error;

@end

RLEVideoDecoder/ExtensionRLEDecoder.h:15 — decoder principal class

@interface ExtensionRLEDecoderFactory : NSObject <MEVideoDecoderExtension>
- (instancetype)init;
- (id<MEVideoDecoder>)videoDecoderWithCodecType:(CMVideoCodecType)codecType
                         videoFormatDescription:(CMVideoFormatDescriptionRef)videoFormatDescription
                     videoDecoderSpecifications:(NSDictionary<NSString *, id> *)videoDecoderSpecifications
             extensionDecoderPixelBufferManager:(MEVideoDecoderPixelBufferManager *)extensionDecoderPixelBufferManager
                                          error:(NSError **)error;
@end
Type Responsibility Depends on or conforms to
ExtensionFormatReaderFactory EXPrincipalClass. Returns an id<MEFormatReader> for each byte source MEFormatReaderExtension
ExtensionFormatReader Parses once, then returns file info, track readers, and metadata MEFormatReader, SimpleContainerReader
ExtensionTrackReader (+ video/audio/timecode subclasses) Track info, duration, data rate. Creates sample cursors METrackReader
ExtensionSampleCursor (+ video/audio/timecode subclasses) Position in a track. Steps and reports sync/dependency info and byte locations MESampleCursor, NSCopying via copyWithZone:
SimpleContainerReader Container parsing and format-description creation over an MEByteSource SimpleContainerFormat.h
ExtensionRLEDecoderFactory EXPrincipalClass. Returns an id<MEVideoDecoder> for each format description MEVideoDecoderExtension
ExtensionRLEDecoder Pixel-buffer attributes, per-frame decode, attachment propagation MEVideoDecoder, RLEDecompressor, MEVideoDecoderPixelBufferManager
RLEDecompressor Pure decode of one XRLE sample into a locked 2vuy pixel buffer CoreMedia, CoreVideo
RLEMovieExporter Orchestrates read → compress → write for both output formats AssetVideoReader, AssetAudioReader, RLECompressor, AssetSampleWriter, SimpleContainerWriter
MovieModel App state, drop handling, export start/cancel, progress hop NSObject, DropDelegate, RLEMovieExporterDelegate

Observed: MovieModel subclasses NSObject only to satisfy RLEMovieExporterDelegate <NSObject>, as its own comment says (FormatReaderAndDecoder/MovieModel.swift:12). The track readers and cursors use Objective-C subclassing for media-type differences. ExtensionFormatReader.setupFileInfo also tests exact classes with [meTrack class] == [ExtensionVideoTrackReader class] (SimpleContainerReader/ExtensionFormatReader.m:134).

Access control

Symbol Access Verified effect Likely rationale
MovieModel.registration Swift private let Only MovieModel can await or replace the registration task (FormatReaderAndDecoder/MovieModel.swift:32) Inference: other code must go through the drop path, which awaits it.
ContentView.movieModel, isChoosingExportDestination Swift private (@State) Only ContentView reaches the storage (FormatReaderAndDecoder/ContentView.swift:13) Inference: the view owns the model’s lifetime.
MovieSavePanelModifier, destinationFromSavePanel() Swift private The file exposes only the View.movieSavePanel(...) extension method (FormatReaderAndDecoder/MovieSavePanel.swift:15, FormatReaderAndDecoder/MovieSavePanel.swift:38, FormatReaderAndDecoder/MovieSavePanel.swift:62) Inference: keep the AppKit modal detail behind one SwiftUI-style API.
exporter, inputURL, progressValue Swift implicit internal (no private(set)) Any app-module code may write them Inference: simplicity. Mutation discipline is by convention.
RLEMovieExporter ivars Objective-C @interface { ... } ivars with default @protected visibility Not exposed as properties. Swift sees only the declared methods and the delegate property (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.h:28) Inference: keep pipeline state inside the Objective-C object.
RLEDecompressRow, RLEDecompressAllRows C static Internal linkage. Visible only inside RLEDecompressor.m (RLEVideoDecoder/RLEDecompressor.m:15, RLEVideoDecoder/RLEDecompressor.m:16) Inference: hide codec internals behind the single decodeFrame:toBuffer: method.
Extension headers Objective-C header boundary Extension headers are not in the app’s bridging header, and no target imports another target’s headers Inference: each .appex is a separate binary. The header boundary matches the process boundary.

Objective-C headers are not equivalent to Swift privacy. Every method declared in an @interface can be messaged by any code that imports the header. In this project, cross-target isolation comes from build membership, not from access modifiers.

Reference code

FormatReaderAndDecoder/MovieSavePanel.swift:15 — private modifier type behind an internal View extension

private struct MovieSavePanelModifier: ViewModifier {
    @Binding var isPresented: Bool
    let defaultFilename: String
    let contentTypes: [UTType]
    let onCompletion: (URL, UTType) -> Void
    // ...
}

Logic ownership and placement

Logic Owning type or file Placement rationale
Opting the app into installed media extensions MovieModel.registration Observed: VTRegisterProfessionalVideoWorkflowVideoDecoders() and MTRegisterProfessionalVideoWorkflowFormatReaders() run once, detached. The comment says the app opts into every installed extension, not only its own (FormatReaderAndDecoder/MovieModel.swift:27).
Container parsing and byte access SimpleContainerReader Observed: reads through MEByteSource readDataOfLength:fromOffset:toDestination:bytesRead:error: (SimpleContainerReader/SimpleContainerReader.m:802), not through file paths. The URL/NSFileHandle path exists but no source calls initWithFileURL:.
Codec identity on read SimpleContainerReader.parseVideoTrackHeader: Observed: creates the video CMVideoFormatDescription from the file’s stored formatID (SimpleContainerReader/SimpleContainerReader.m:617).
Sample byte location (not bytes) ExtensionVideoSampleCursor.sampleLocationReturningError: Observed: returns MESampleLocation (byte source + offset/length) (SimpleContainerReader/ExtensionSampleCursor.m:274). The extension’s own block-buffer read method has no caller.
Synthesized timecode ExtensionTimecodeSampleCursor Observed: builds a one-sample CMSampleBuffer itself (SimpleContainerReader/ExtensionSampleCursor.m:416), because the container stores no timecode.
Codec acceptance ExtensionRLEDecoder init / canAcceptFormatDescription: Observed: rejects any codec other than kRLECodecCode. Accepts a new format description only when the codec and dimensions match (RLEVideoDecoder/ExtensionRLEDecoder.m:57, RLEVideoDecoder/ExtensionRLEDecoder.m:128).
Pixel decode RLEDecompressor.decodeFrame:toBuffer: Observed: makes the data contiguous, checks the 4-byte size header, and decodes into a locked 2vuy buffer (RLEVideoDecoder/RLEDecompressor.m:48, RLEVideoDecoder/RLEDecompressor.m:73).
Color attachments ExtensionRLEDecoder.addAttachmentsFromFormatDescription:toPixelBuffer: Observed: copies format extensions to the pixel buffer and falls back to Rec. 709. At RLEVideoDecoder/ExtensionRLEDecoder.m:206 the fallback passes the key constant kCMFormatDescriptionExtension_YCbCrMatrix as the matrix value.
Codec identity on write RLEMovieExporter.newVideoFormatDescriptionFromSource: Observed: stamps kRLECodecCode on the output format description (FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m:527). The Simple Container writer stores that code as the track’s formatID (FormatReaderAndDecoder/RLEMovieExporter/SimpleContainerWriter.m:419).

Design patterns

Pattern Source evidence Purpose or tradeoff
Factory SimpleContainerReader/ExtensionFormatReader.m:22 (formatReaderWithByteSource:options:error:), RLEVideoDecoder/ExtensionRLEDecoder.m:23 (videoDecoderWithCodecType:...). Media-type subclass selection in SimpleContainerReader/ExtensionTrackReader.m:106 and SimpleContainerReader/ExtensionFormatReader.m:102 The framework depends only on the MEFormatReaderExtension/MEVideoDecoderExtension protocols. The extension supplies the concrete products. Per-file and per-stream instances keep state out of the long-lived principal class.
Delegate or data-source callbacks FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.h:22 (RLEMovieExporterDelegate, weak at FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.h:42). FormatReaderAndDecoder/MovieModel.swift:66 (DropDelegate) The Objective-C pipeline reports progress without knowing about SwiftUI. The weak reference prevents an owner ↔︎ exporter cycle.
Binding-based state propagation FormatReaderAndDecoder/MovieSavePanel.swift:16 (@Binding var isPresented), FormatReaderAndDecoder/ContentView.swift:53 The modifier consumes a one-shot trigger and resets it, while ContentView stays the owner.

Removed from the draft: “Protocol-oriented abstraction”. The protocols that shape the design (MEFormatReader, METrackReader, MESampleCursor, MEVideoDecoder, and their extension protocols) are framework contracts that the sample conforms to. They are not abstractions the sample designs or substitutes across. Its only local protocol is the single-method exporter delegate, and its variation uses Objective-C subclassing.

Main application flow

Playing a dropped .simpl movie: the scope is the source-visible handoffs. The system’s internal routing is shown as one participant.

Reference code

SimpleContainerReader/ExtensionSampleCursor.m:264 — the reader hands back where the bytes are, not the decoded or even the compressed frame

- (MESampleLocation *)sampleLocationReturningError:(NSError **)error {
    int err;
    uint64_t offset, size;
    AVSampleCursorStorageRange storageRange;
    MESampleLocation *sampleLocation = nil;

    err = [m_reader getVideoSampleInfoAtTime:m_pts sampleOffset:&offset sampleSize:&size];
    if (err == noErr) {
        storageRange.offset = offset;
        storageRange.length = size;
        sampleLocation = [[MESampleLocation alloc] initWithByteSource:[m_reader byteSource] sampleLocation:storageRange];
    } else if (error != nil) {
        *error = [NSError errorWithDomain:MediaExtensionErrorDomain code:MEErrorLocationNotAvailable userInfo:nil];
    }

    return sampleLocation;
}

RLEVideoDecoder/ExtensionRLEDecoder.m:84 — the decoder receives a CMSampleBuffer and fills a system-managed pixel buffer

    if (sampleBuffer != NULL && m_decoder != nil && ![options doNotOutputFrame]) {
        // Create the output pixel buffer.
        outputPixelBuffer = [m_pixelBufferManager createPixelBufferAndReturnError:&error];
        // Decode the frame.
        err = [m_decoder decodeFrame:sampleBuffer toBuffer:outputPixelBuffer];
        if (err == noErr) {
            // ...
        } else {
            // ...
        }
    } else {
        // ...
    }

Reader → decoder data flow, as source-visible facts. (1) The exporter writes kRLECodecCode as the video track formatID. (2) The reader turns formatID into the track’s CMVideoFormatDescription codec type. (3) The reader returns sample byte ranges in an MESampleLocation. (4) The decoder’s Info.plist claims CodecType XRLE, and its factory re-checks kRLECodecCode. (5) The decoder consumes a CMSampleBuffer whose data begins with a 4-byte big-endian size (read in RLEDecompressor’s decodeFrame:toBuffer:). Inference: the system reads the located bytes, wraps them in a sample buffer with the reader’s format description, and selects the decoder by codec type. No sample code performs those steps. The same inference explains why an exported XRLE QuickTime movie needs only the decoder extension.

Naming conventions

  • Types: the Extension… prefix marks classes that conform to MediaExtension protocols (ExtensionFormatReader, ExtensionTrackReader, ExtensionSampleCursor, ExtensionRLEDecoder). The …Factory suffix marks the EXPrincipalClass types. Media-type subclasses insert the medium (ExtensionVideoTrackReader, ExtensionTimecodeSampleCursor). Helpers without the prefix are format or codec logic with no framework role (SimpleContainerReader, RLEDecompressor, RLECompressor, SimpleContainerWriter). App-side Swift uses …Model, …View, and …Modifier.
  • Protocols: the one local protocol uses the Cocoa …Delegate suffix (RLEMovieExporterDelegate).
  • Methods: Objective-C methods follow Cocoa conventions: copy…/new… return retained Core Foundation objects (copyFormatDescriptionForMediaType:, newVideoFormatDescriptionFromSource:), …ReturningError: uses an error out-parameter, load…WithCompletionHandler: delivers asynchronously, and init… initializes. Container-parser methods return C int status codes. Swift methods are commands (exportMovie(to:as:), cancelMovieWriting()).
  • Instance variables: an m_ prefix in Objective-C (m_reader, m_pixelBufferManager). The exporter’s ivars have no prefix.
  • Identifiers and files: one {h,m} pair per primary class, with media-type subclasses in the same pair. Constants use k plus four-character codes (kRLECodecCode 'XRLE', kSimpleContainerMovieID 'smpl'). Bundle and type identifiers share the com.example.apple-samplecode. prefix.

Architecture takeaways

  • A MediaExtension feature is a set of independent binaries joined by declarative metadata (EXPrincipalClass, UTTypes, CodecType, entitlements) and shared data contracts. Keep cross-target coupling to headers that define formats, as Shared/ does.
  • Make the principal class a thin factory and put state in per-file or per-stream objects. In this sample the reader object owns the parser, and every track and cursor shares that parser.
  • Readers describe and locate. Decoders transform. The reader returns MESampleLocation byte ranges and a format description whose codec type selects the decoder. It never decodes and never calls the decoder.
  • On the host side, keep blocking framework calls (…Register…, synchronous export) off the main actor, and hop back with Task { @MainActor in } before mutating observable state. Without actor annotations on the model, that discipline is by convention, not checked by the compiler.
  • For cancellation across a Swift/Objective-C boundary, a C11 atomic flag polled by the work loop is a minimal, explicit mechanism. Note that the sample’s Swift task handle is not retained, so the task itself is never cancelled.

Source map

Source file Relevant symbols
FormatReaderAndDecoder/ContentView.swift ContentView, @State movieModel, onDrop, movieSavePanel
FormatReaderAndDecoder/MovieModel.swift MovieModel, registration, exportMovie(to:as:), performDrop(info:), progressChanged(_:), UTType.simpleContainerFormat
FormatReaderAndDecoder/MovieSavePanel.swift MovieSavePanelModifier, destinationFromSavePanel(), movieSavePanel(...)
FormatReaderAndDecoder/Info.plist UTExportedTypeDeclarations (com.example.apple-samplecode.simpleformat)
FormatReaderAndDecoder/RLEMovieExporter/FormatReaderAndDecoder-Bridging-Header.h Bridging import
FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.h RLEMovieExporterDelegate, RLEMovieExporter, stopWriting
FormatReaderAndDecoder/RLEMovieExporter/RLEMovieExporter.m writeRLEQuickTimeMovie:, writeRLESimpleFormatMovie:, cancelWriting, finishWriting, newVideoFormatDescriptionFromSource:
FormatReaderAndDecoder/RLEMovieExporter/AssetVideoReader.m m_getBufferSync, copySampleBufferForTime:
FormatReaderAndDecoder/RLEMovieExporter/AssetAudioReader.m copySampleBufferForTime:sampleCount:
FormatReaderAndDecoder/RLEMovieExporter/AssetSampleWriter.m m_semaphore, finishWriting
FormatReaderAndDecoder/RLEMovieExporter/SimpleContainerWriter.h SimpleContainerWriter
FormatReaderAndDecoder/RLEMovieExporter/SimpleContainerWriter.m Video track formatID
SimpleContainerReader/Info.plist EXPrincipalClass, MTUTTypeArray, MTFileNameExtensionArray
SimpleContainerReader/SimpleContainerReader.entitlements com.apple.developer.mediaextension.formatreader
SimpleContainerReader/ExtensionFormatReader.h ExtensionFormatReaderFactory, ExtensionFormatReader
SimpleContainerReader/ExtensionFormatReader.m Factory method, eager setup, load… methods
SimpleContainerReader/ExtensionTrackReader.m ExtensionTrackReader and subclasses, cursor creation
SimpleContainerReader/ExtensionSampleCursor.m ExtensionSampleCursor and subclasses, sampleLocationReturningError:
SimpleContainerReader/SimpleContainerReader.h SimpleContainerReader
SimpleContainerReader/SimpleContainerReader.m readDataOfLength:fromOffset:, parseVideoTrackHeader:
RLEVideoDecoder/Info.plist EXPrincipalClass, CodecInfo / CodecType
RLEVideoDecoder/RLEVideoDecoder.entitlements com.apple.developer.mediaextension.videodecoder
RLEVideoDecoder/ExtensionRLEDecoder.h ExtensionRLEDecoderFactory, ExtensionRLEDecoder
RLEVideoDecoder/ExtensionRLEDecoder.m Factory method, decodeFrameFromSampleBuffer:, canAcceptFormatDescription:, attachments
RLEVideoDecoder/RLEDecompressor.h RLEDecompressor
RLEVideoDecoder/RLEDecompressor.m decodeFrame:toBuffer:, RLEDecompressRow, RLEDecompressAllRows
Shared/SimpleContainerFormat.h SimpleMovieHeader, SimpleVideoTrackInfo
Shared/RLECodec.h kRLECodecCode
FormatReaderAndDecoder.xcodeproj/project.pbxproj Targets, embed phase, dependencies, bridging header, Swift version