Writing fragmented MPEG-4 files for HTTP Live Streaming
At a glance
| Item | Summary |
|---|---|
| Purpose | Convert one movie into fragmented MPEG-4 initialization/media segments and an HLS media playlist. |
| App architecture | A command-line orchestrator wires track loading, a reader/writer segment producer, and two Combine consumers for files and playlist text. |
| Main patterns | Reactive fan-out pipeline, AVAssetWriter delegate adapter, configuration/value objects, explicit lifetime tokens. |
| Project style | One file per pipeline stage: configuration, loading, segment generation, index reduction, errors, and top-level composition. |
Project structure
fmp4writer/
├── main.swift
├── Configuration.swift
├── TrackLoading.swift
├── SegmentGeneration.swift
├── IndexFileGeneration.swift
└── Error.swift
The flat folder is architectural: files are named for sequential pipeline responsibilities rather than UI features or layers.
Overall architecture
flowchart LR
CLI["main.swift"] --> Config["FMP4WriterConfiguration"]
CLI --> Load["loadTracks"]
Load --> Source["SourceMedia"]
Source --> RW["ReaderWriter"]
RW --> Subject["Segment Subject"]
Subject --> Files["Segment file writer"]
Subject --> Index["Index-file reducer"]
Index --> Playlist["m3u8 writer"]
Reference code
fmp4writer/main.swift:41 — top-level composition fans one segment stream into two sinks (abridged).
let segmentGenerator = PassthroughSubject<Segment, Error>()
let indexFileGenerator = segmentGenerator.reduceToIndexFile(using: config)
let segmentFileWriter = segmentGenerator.tryMap { segment in
let name = segment.fileName(forPrefix: config.segmentFileNamePrefix)
try segment.data.write(to: outputDirectoryURL.appendingPathComponent(name))
}
segmentAndIndexFileWriter = segmentFileWriter.merge(with: indexFileWriter)
.sink(receiveCompletion: { completion in /* finish */ }, receiveValue: {})This is a dataflow-oriented command-line architecture: main.swift composes stages, while stage files own transformations.
Ownership and state
classDiagram
Main *-- FMP4WriterConfiguration
Main *-- ReaderWriter : retained token
Main *-- AnyCancellable : retained sink
ReaderWriter *-- AVAssetReader
ReaderWriter *-- AVAssetWriter
ReaderWriter *-- AVAssetReaderOutput
ReaderWriter *-- AVAssetWriterInput
ReaderWriter o-- Subject : supplied
Ownership evidence
fmp4writer/main.swift:20 and fmp4writer/SegmentGeneration.swift:35 — top-level tokens retain the async graph; ReaderWriter owns AVFoundation components (abridged).
var segmentGenerationToken: Any?
var segmentAndIndexFileWriter: AnyCancellable?
class ReaderWriter<S>: NSObject, AVAssetWriterDelegate
where S: Subject, S.Output == Segment, S.Failure == Error {
private let assetReader: AVAssetReader
private let assetWriter: AVAssetWriter
private let subject: S
private let audioReaderOutput: AVAssetReaderOutput
private let videoReaderOutput: AVAssetReaderOutput
}| Owner | Object or state | Relationship | Mutation authority |
|---|---|---|---|
Top-level main.swift |
Configuration, dispatch group, completion, tokens | Process-lifetime orchestration | Completion sink writes result and leaves the group. |
ReaderWriter |
Reader/writer, inputs/outputs, index | Creates and retains | Private methods and writer delegate callback. |
PassthroughSubject |
Segment event stream | Created in main, supplied to producer/consumers | ReaderWriter sends; downstream operators observe. |
IndexFileState |
Playlist reduction accumulator | Value copied by Combine scan | Reducer alone produces new state. |
Class and protocol design
classDiagram
class Subject {
<<protocol>>
}
class AVAssetWriterDelegate {
<<protocol>>
}
ReaderWriter ..|> AVAssetWriterDelegate
ReaderWriter --> Subject : generic output
PassthroughSubject ..|> Subject
Segment --> IndexFileState : reduced into
Reference code
fmp4writer/SegmentGeneration.swift:19 — the producer is generic over Combine’s capability rather than a concrete subject (abridged).
func generateSegments<S>(sourceMedia: SourceMedia,
configuration: FMP4WriterConfiguration,
subject: S) -> Any?
where S: Subject, S.Output == Segment, S.Failure == Error {
let readerWriter = try ReaderWriter(
sourceMedia: sourceMedia,
configuration: configuration,
subject: subject
)
readerWriter.start()
return readerWriter
}The generic boundary is protocol-oriented but narrow: callers choose any matching Subject; ReaderWriter adapts AVAssetWriterDelegate callbacks into Segment values.
Access control
| Symbol | Access | Verified effect | Likely rationale |
|---|---|---|---|
ReaderWriter AV objects, subjects, completion state |
private |
Other files cannot disturb read/write lifecycle. | Maintains paired reader/writer and completion invariants. |
| Configuration and value-object fields | internal immutable let |
Pipeline stages can read fixed settings but not mutate them. | Small single-target configuration contract. |
| Stage functions/types | internal default | main.swift composes them across files. |
The executable target is the module boundary. |
| Top-level tokens | file top-level internal variables | Keep async producer/subscription alive until group completion. | Lifetime retention is explicit, not hidden in a singleton. |
fmp4writer/SegmentGeneration.swift:35 (abridged):
class ReaderWriter<S>: NSObject, AVAssetWriterDelegate
where S: Subject, S.Output == Segment, S.Failure == Error {
private let assetReader: AVAssetReader
private let assetWriter: AVAssetWriter
private let audioDone = PassthroughSubject<Void, Error>()
private let videoDone = PassthroughSubject<Void, Error>()
private var done: AnyCancellable?
private var segmentIndex = 0
}Logic ownership and placement
| Logic | Owning type or file | Placement rationale |
|---|---|---|
| CLI parsing and pipeline wiring | fmp4writer/main.swift |
Composition root only; media details stay in stages. |
| Track loading/validation | fmp4writer/TrackLoading.swift |
Produces validated SourceMedia. |
| Read/transcode/segment callbacks | fmp4writer/SegmentGeneration.swift |
Keeps AVAssetReader/Writer lifecycle together. |
| Playlist accumulation | fmp4writer/IndexFileGeneration.swift |
Pure-ish Combine reduction from segment reports to text. |
| Encoding constants | fmp4writer/Configuration.swift |
One immutable object feeds every stage. |
Design patterns
| Pattern | Source evidence | Purpose or tradeoff |
|---|---|---|
| Reactive fan-out pipeline | fmp4writer/main.swift:43 |
One segment stream drives file output and playlist generation. |
| Delegate adapter | fmp4writer/SegmentGeneration.swift:124 |
Converts AVAssetWriter segment callbacks into typed values. |
| Configuration object | fmp4writer/Configuration.swift:11 |
Groups paths, encoding, segmentation, and timing settings. |
| Value object | fmp4writer/SegmentGeneration.swift:11 |
Moves segment bytes/report/identity between producer and consumers. |
| Parallel workers + join | fmp4writer/SegmentGeneration.swift:74 |
Audio/video transfer separately; Combine completion finishes only after both. |
Naming conventions
- Files and functions name pipeline stages (
TrackLoading,generateSegments,reduceToIndexFile). - Data carriers are nouns (
SourceMedia,Segment,IndexFileState,FMP4WriterConfiguration). ReaderWriternames its paired framework ownership; private properties retain framework vocabulary.- Configuration fields describe output artifacts (
segmentFileNamePrefix,indexFileName) and constraints.
Architecture takeaways
- Make the executable entry point a composition root and keep transformations in stage files.
- Adapt framework delegate callbacks into a typed stream when multiple downstream consumers need the same events.
- Retain asynchronous producers and subscriptions explicitly for their required lifetime.
- Use private ownership inside the stateful reader/writer and immutable values between stages.
Source map
| Source file | Relevant symbols |
|---|---|
fmp4writer/main.swift |
Composition root and output sinks |
fmp4writer/Configuration.swift |
FMP4WriterConfiguration |
fmp4writer/TrackLoading.swift |
SourceMedia, track validation |
fmp4writer/SegmentGeneration.swift |
Segment, ReaderWriter, delegate adapter |
fmp4writer/IndexFileGeneration.swift |
Playlist reducer/state |
Source caveat: the archive uses callback-based AVFoundation loading and Combine. This document describes that source architecture without inferring a newer implementation.