Featured image of post Fix split adaptation set in MPEG-DASH

Fix split adaptation set in MPEG-DASH

I have been writing a program to automatically transcode and package videos for adaptive bitrate streaming in MPEG-DASH format.

The program seemed to be handling all video files correctly, generating all the mpd manifests and segment files as expected, until I encountered that one specific video file (classic, right?).

The video file did not throw error or anything during the transcoding and packaging process, but when I implemented the bitrate switching functionality in the frontend, the issue presented itself:

Adaptive bitrate switching

For this particular video, there should be 720p, 480p, and 360p resolutions available, but the video player only detected 480p.

tl;dr

The issue was caused by MP4Box treating the 480p video incompatible with the other resolutions due to the Display Aspect Ratio.

Adding the option -vf "setdar=<aspect-ratio>" solved the issue. For example:

1
"-vf", "scale=-2:720,setsar=1,setdar=16/9"

Manifest file

Naturally, the first thing I did was to check the generated manifest file. Something interesting was happening there:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  <!-- ... -->
  <AdaptationSet segmentAlignment="true" maxWidth="854" maxHeight="480" maxFrameRate="11988/500" par="427:240" startWithSAP="1">
    <Representation id="480p" mimeType="video/mp4" codecs="avc1.4D401E" width="854" height="480" frameRate="11988/500" sar="1:1" bandwidth="988684">
    </Representation>
    <!-- ... -->
  <AdaptationSet segmentAlignment="true" maxWidth="1280" maxHeight="720" maxFrameRate="11988/500" par="16:9" startWithSAP="1">
    <Representation id="360p" mimeType="video/mp4" codecs="avc1.4D401E" width="640" height="360" frameRate="11988/500" sar="1:1" bandwidth="227124">
    </Representation>
    <Representation id="original" mimeType="video/mp4" codecs="avc1.4D401F" width="1280" height="720" frameRate="11988/500" sar="1:1" bandwidth="1438533">
    </Representation>
    <!-- ... -->
  </AdaptationSet>
  <!-- ... -->

It took me a while to realize that the par (Display Aspect Ratio) was different for the two adaptation sets. The 480p video had par="427:240" while the others had par="16:9", which made them incompatible.

Info

For most of the other videos with the same resolution, ffmepg automatically generated the par value as 16:9. I’m still not sure why this video along with a few others had a different par value only for 480p.

Fixing the code

I was using a Go function like below to transcode the video:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func transcodeVideo(src string, dst string, maxFps float64, height int, maxRate string, bufSize string, preset string) error {
    keyint := int(maxFps * 2)
    _, err := runCmd(*exec.Command("ffmpeg", "-y", "-i", src, "-c:v", "libx264",
        "-r", fmt.Sprintf("%f", maxFps), "-x264opts", fmt.Sprintf("keyint=%d:min-keyint=%d:no-scenecut", keyint, keyint),
        "-vf", fmt.Sprintf("scale=-2:%d,setsar=1", height),
        "-b:v", fmt.Sprintf("%s", maxRate),
        "-maxrate", maxRate, "-movflags", "faststart", "-bufsize", bufSize,
        "-preset", preset, "-profile:v", "main", "-an", "-pix_fmt", "yuv420p", dst))
    if err != nil {
        return fmt.Errorf("Error running ffmpeg: %v", err)
    }
    return nil
}

And the fix was simple: ffmpeg supports setting the Display Aspect Ratio (DAR) explicitly with the setdar filter.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func transcodeVideo(src string, dst string, maxFps float64, height int, dar string, maxRate string, bufSize string, preset string) error {
    keyint := int(maxFps * 2)
    _, err := runCmd(*exec.Command("ffmpeg", "-y", "-i", src, "-c:v", "libx264",
        "-r", fmt.Sprintf("%f", maxFps), "-x264opts", fmt.Sprintf("keyint=%d:min-keyint=%d:no-scenecut", keyint, keyint),
        "-vf", fmt.Sprintf("scale=-2:%d,setsar=1,setdar=%s", height, dar), // HERE
        "-b:v", fmt.Sprintf("%s", maxRate),
        "-maxrate", maxRate, "-movflags", "faststart", "-bufsize", bufSize,
        "-preset", preset, "-profile:v", "main", "-an", "-pix_fmt", "yuv420p", dst))
    if err != nil {
        return fmt.Errorf("Error running ffmpeg: %v", err)
    }
    return nil
}

The dar parameter comes from the aspect ratio of the source video, simplified using a simple Euclidean algorithm.

1
2
3
// driver
rw, rh := util.SimplifyRatio(vtrack.Width, vtrack.Height)
dar := fmt.Sprintf("%d/%d", rw, rh)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// util
func gcd(a, b int) int {
	for b != 0 {
		a, b = b, a%b
	}
	return a
}

func SimplifyRatio(width, height int) (int, int) {
	div := gcd(width, height)
	return width / div, height / div
}

Fixed

Feels good.

Adaptive bitrate switching fixed

Built with Hugo
Theme Stack designed by Jimmy