DEV Community

MaximKomlev
MaximKomlev

Posted on AI-assisted

Playing MPEG-DASH with Native AVPlayer on iOS — Building UPlayer

How I built an open-source Swift framework that converts MPEG-DASH into an AVPlayer-compatible HLS representation directly on the client.

Apple's AVPlayer provides a powerful foundation for media playback on iOS. It supports hardware-accelerated decoding, adaptive streaming, Picture in Picture, and integration with native playback controls.

However, there is one important limitation: AVPlayer does not natively support MPEG-DASH.

For applications that need to play DASH streams, the usual options are to integrate another playback engine or introduce server-side conversion to HLS.

I wanted to explore a different approach:

What if we could adapt MPEG-DASH streams to AVPlayer without replacing the native playback engine or modifying the streaming backend?

That experiment became UPlayer, an open-source Swift framework for MPEG-DASH, HLS, and MP4 playback.

The Idea: Convert DASH to HLS on the Client

MPEG-DASH and HLS solve similar problems. Both describe media representations and segments that a player can retrieve over HTTP.

However, their manifest formats and playback integration differ.

DASH typically uses an XML-based Media Presentation Description (MPD), while HLS uses M3U8 playlists.

Instead of decoding and rendering media independently, UPlayer translates the DASH manifest and media references into an HLS representation that AVPlayer can understand.

The conversion happens inside the application.

This allows UPlayer to retain native playback functionality while introducing a separate layer responsible for media compatibility.

Architecture: A Virtual Streaming Server Inside the Application

The central architectural idea is to move part of the streaming-server functionality into the client.

UPlayer downloads and parses manifests, prepares media representations, generates virtual HLS playlists, and resolves requests through a custom AVAssetResourceLoader.

The overall architecture looks like this:

UPlayer Architecture

The generated HLS representation is virtual. UPlayer does not need to launch a local HTTP server or require the backend to provide an additional HLS stream.

When the media is compatible with Apple's playback stack, the original encoded video segments can be reused without decoding and re-encoding them.

This is an important distinction: UPlayer adapts the streaming representation rather than implementing its own video decoder or renderer.

A Modular Processing Pipeline

I wanted the media-processing architecture to remain extensible rather than becoming a large, tightly coupled player implementation.

UPlayer uses independent processors coordinated by UPlayerAssetProcessorsQueue.

Each processor performs a specific operation on a shared media asset.

Some of the main components include:

Component Responsibility
UPlayerMetadataDownloader Downloads media metadata and identifies the source type
UPlayerMPDParser Parses MPEG-DASH manifests
UPlayerThumbnailDownloader Processes thumbnail metadata and sprite resources
UPlayerHLSGenerator Generates HLS playlists from DASH segment templates
UPlayerSegmentBaseHLSGenerator Handles byte-range-based DASH representations
UPlayerMPDToMP4Resolver Provides an MP4 fallback path
UPlayerAVAssetResourceLoader Serves virtual media resources to AVPlayer

Processors share a common interface:

protocol UPlayerAssetProcessorProtocol {
    func process(
        asset: UPlayerAssetProtocol
    ) -> AnyPublisher<UPlayerAssetProtocol, Error>
}
Enter fullscreen mode Exit fullscreen mode

Using Combine allows the processing stages to be composed while maintaining a consistent asset representation.

The benefit is that individual stages can evolve independently.

For example, supporting another DASH segment addressing mode does not require redesigning the playback engine.

Connecting Virtual HLS to AVPlayer

Generating an HLS playlist is only part of the problem.

AVPlayer must also be able to request the playlist and its referenced media resources.

UPlayer uses a custom URL scheme:

uplayer://...
Enter fullscreen mode Exit fullscreen mode

These requests are handled by UPlayerAVAssetResourceLoader, which maps the virtual resources to generated playlists, downloaded media, or cached content.

Conceptually, the process looks like this:

Modular Processing Flow diagram

This separation is central to the design.

UPlayer manages media preparation and resource resolution. AVPlayer remains responsible for playback, decoding, timing, and rendering.

Supporting Live MPEG-DASH

Video-on-demand content has a relatively stable segment timeline.

Live streaming is more complicated because the available media window changes continuously.

UPlayer includes support for dynamic MPDs and live playlist generation.

The live processing logic handles:

  • MPD refresh based on manifest update information.
  • Sliding HLS playlist windows.
  • Playlist merging to maintain continuity.
  • Live-edge positioning with a configurable segment offset.

The objective is to maintain a continuously updated HLS representation as the underlying DASH stream advances.

This is especially important because simply regenerating a playlist without accounting for segment continuity can disrupt playback.

Caching and Offline Playback

Media caching is another important part of the architecture.

UPlayer can cache manifests and media resources, allowing downloaded content to be reused across playback requests.

Caching is integrated with resource loading, so AVPlayer can consume previously downloaded data without requiring a separate player implementation.

This provides several practical benefits:

  • Reduced repeated network downloads.
  • Faster access to already cached media.
  • Prefetching of media resources.
  • Offline playback when the necessary content is fully cached.

The cache also supports lifecycle management, including size limits, expiration policies, and least-recently-used eviction.

On-Demand G.711 to AAC Transcoding

Not every media source uses an audio format that AVPlayer can consume in the required streaming configuration.

One example is G.711, commonly encountered in communication and camera streaming systems.

UPlayer supports pluggable audio transcoding, including converting G.711 into AAC when required.

The processing flow is straightforward:

UPlayer Architecture

The important architectural decision is that transcoding is optional and performed only when needed.

Compatible audio does not need to pass through an unnecessary transcoding stage.

The transcoding implementation is also separate from the main playback pipeline, allowing additional formats to be supported independently.

Thumbnail Previews and Media Export

UPlayer also includes features beyond basic playback.

For DASH sources containing image adaptation sets, the framework can process tiled thumbnail sprites and provide previews during timeline scrubbing.

The repository also includes UPlayerMediaExporter, which supports exporting processed DASH, HLS, and MP4 media to local files.

For supported sources, the exporter can download media segments, handle audio compatibility when necessary, and mux the resulting tracks into an MP4 file.

These capabilities reuse the same general principle: process media at the application layer while avoiding unnecessary decoding or transcoding.

Using UPlayer in Swift

The framework exposes a simple playback interface.

For example:

import UPlayer

let player = UPlayer()

let url = URL(
    string: "https://example.com/manifest.mpd"
)!

player.play(url: url)
Enter fullscreen mode Exit fullscreen mode

Playback can be stopped with:

player.stop()
Enter fullscreen mode Exit fullscreen mode

Applications can also use UPlayerDelegate to observe playback-related events and integrate the framework with custom rendering interfaces.

The goal is to keep application-level usage straightforward while hiding the complexity of manifest parsing, playlist generation, and resource resolution.

Why Keep AVPlayer?

The main motivation behind this project was not to build another media player.

It was to preserve Apple's native playback capabilities while supporting streaming formats that AVPlayer does not directly understand.

By keeping AVPlayer as the playback engine, applications can continue using:

  • Hardware-accelerated video decoding.
  • Native Picture in Picture.
  • AVPlayerViewController.
  • System playback and background-media integration.
  • Custom playback interfaces built around AVFoundation.

The media-processing layer can evolve independently from the native player.

That separation is what makes this architecture interesting to me.

Final Thoughts

UPlayer began as an experiment in adapting media formats rather than replacing playback technology.

By translating MPEG-DASH into a virtual HLS representation, the framework makes it possible to use native AVPlayer for DASH playback without requiring server-side conversion.

The modular architecture also provides room for features such as live playlist management, persistent caching, thumbnail processing, audio transcoding, and media export.

If you're working with AVFoundation, MPEG-DASH, HLS, or custom media pipelines on Apple platforms, you may find the implementation useful.

Source code: UPlayer on GitHub

The project is open source, and feedback, contributions, and technical discussions are welcome.


Originally published on Medium.

Top comments (0)