Skip to content

Creating an encoding ladder for the video lab

For downloading playback logs after a trial, see Collect metrics below.

Use HAS_encode.py to turn the supplied 120-second video into a playable DASH package. A rung is one encoded version of the video with its own resolution, frame rate, and bitrate. The collection of rungs is your encoding ladder. During playback, the DASH client can switch between these versions as network conditions change.

Requirements

You need Python 3, FFmpeg with the libx264 encoder, and ffprobe available on your command path. Use the supplied two-minute MP4 with audio as the input, not the full movie. The script encodes the first 120 seconds.

Optional local setup: Linux, macOS, or Windows

The encoding script uses only Python's standard library, so there are no pip dependencies and no requirements.txt installation step. FFmpeg is an external program, not a Python package. Installing a Python package named ffmpeg does not satisfy this requirement.

Requirement What to install or provide
Python A currently supported Python 3 release, e.g. 3.11 or newer
FFmpeg A build with libx264 video encoding, AAC audio encoding, and the DASH muxer
ffprobe The executable included with FFmpeg; both commands must be on PATH
Lab files HAS_encode.py and the unchanged supplied bbb-first-120s.mp4
Storage Writable output location with room for both intermediate MP4 files and the final DASH package

On Debian/Ubuntu:

sudo apt-get update
sudo apt-get install python3 ffmpeg

On macOS, install Python from python.org and a compatible FFmpeg build for your processor. The FFmpeg download page links to compiled builds and platform packages. Ensure ffmpeg and ffprobe are on your PATH.

On Windows, install Python from python.org and a Windows FFmpeg build linked from the same FFmpeg download page. Extract FFmpeg and add its bin directory (containing ffmpeg.exe and ffprobe.exe) to your user PATH. Open a new PowerShell window after changing PATH.

Check the installation without encoding anything:

ffmpeg -version
ffprobe -version
ffmpeg -hide_banner -encoders
ffmpeg -hide_banner -muxers

Look for libx264 and aac in the encoder list and dash in the muxer list. Check Python with python3 --version on Linux/macOS or py -3 --version on Windows (use python --version if that is your installed Python command).

Put the script and video in one directory and open a terminal there. Linux/macOS:

python3 HAS_encode.py bbb-first-120s.mp4 submission-A --rung 1280x720:30:1200 --rung 1920x1080:60:3000

Windows PowerShell:

py -3 .\HAS_encode.py .\bbb-first-120s.mp4 .\submission-A --rung 1280x720:30:1200 --rung 1920x1080:60:3000

Use a single line as above: the Bash \ line continuations in other examples do not work in PowerShell. Quote paths containing spaces. The output directory must be new. The script uses the platform's null device for the first encoding pass and invokes FFmpeg without a shell, so its encoding workflow is intended to work on all three systems. Windows and macOS execution have not been verified in this project. FFmpeg/libx264 versions can affect sizes and quality; record versions when comparing results across machines.

Local encoding does not require Docker or GNS3. Transfer the entire output directory to your lab VM afterwards, then use upload-stream.sh there. The upload and metrics-export Bash helpers target the Linux VM with access to its Docker daemon; they are not native Windows scripts. The Python analysis script also uses only the standard library and can analyze exported logs locally.

Running on the preconfigured VM

On the VM, start with cd /home/ttm4250. Relative input/output paths are resolved against your current directory, not the script's location. Choose a new output name for each encoding set. If the shared directory is not writable by your account, use a destination under your own home directory instead.

A complete VM workflow is:

cd /home/ttm4250
python3 HAS_encode.py bbb-first-120s.mp4 student-submission \
  --rung 640x360:24:400 --rung 1280x720:30:1200 --rung 1920x1080:60:3000
./upload-stream.sh SERVER_CONTAINER student-submission
# Stop/start the DASH server in GNS3, then play and save trials on the client.
./collect-metrics.sh CLIENT_CONTAINER results
python3 analyze-runs.py results

Replace the container placeholders with actual container IDs or names. The CSV will be results/comparison.csv; raw logs remain under results/metrics. The upload destination is /srv/uploads/student-submission inside the server.

Specify your rungs

Add one --rung argument for each version you want:

--rung WIDTHxHEIGHT:FPS:VIDEO_KBPS
Value Meaning Allowed values
WIDTHxHEIGHT Video resolution in pixels, width first Positive even integers; aspect ratio must match the source
FPS Frames displayed per second 24, 30, or 60
VIDEO_KBPS Target average video bitrate in kilobits per second A positive integer, e.g. 1200 for 1.2 Mbit/s

For example, --rung 1280x720:30:1200 creates a 1280×720 version at 30 FPS, targeting 1200 kbit/s of video. The bitrate excludes the shared audio track. Use numbers without suffixes: write 1200, not 1200k or 1.2M.

This command creates three rungs:

python3 HAS_encode.py \
  bbb-first-120s.mp4 student-submission \
  --rung 640x360:24:400 \
  --rung 1280x720:30:1200 \
  --rung 1920x1080:60:3000

The first positional argument is the source file; the second is a new output directory. Existing directories are rejected to prevent overwriting files. For another attempt, choose a different name, such as student-trial-2.

Supplying any --rung arguments replaces the entire default ladder. Without them, the script uses:

Resolution FPS Video bitrate
1280×720 60 1400 kbit/s
1920×1080 60 2800 kbit/s
2560×1440 60 4800 kbit/s

Choose values deliberately

Higher resolution preserves more spatial detail, while higher FPS preserves more motion detail. Both require sufficient bitrate to produce a useful result. A larger resolution at a low bitrate is not automatically better than a smaller resolution with fewer compression artifacts.

You can keep resolution fixed and vary FPS or bitrate between rungs. Exact duplicates of all three settings are rejected. To investigate one variable, change that variable while holding the others constant across experiments.

Reducing FPS or resolution does not automatically reduce the file size when the target bitrate stays the same. Lower the bitrate as well if your intention is to save storage and reduce network demand. Bitrate is a target average; individual segments can require more or less bandwidth.

Aspect-ratio and input checks

The supplied 3840×2160 video has a 16:9 aspect ratio. Suitable dimensions include 640×360, 960×540, 1280×720, 1920×1080, and 2560×1440.

Before encoding, the script checks every rung against the source's display aspect ratio, including its pixel aspect ratio. For example, 854×480 does not exactly match 16:9 and is rejected. The error identifies the rung and the required ratio. This stops the job rather than allowing an incompatible encode to continue; the script does not automatically crop or pad the video.

Resolution and FPS cannot exceed the source. Unsupported FPS values, odd dimensions, nonpositive bitrates, duplicate rungs, a source without audio, and a source duration outside 120 ± 0.1 seconds are also rejected.

Follow any additional course limits on maximum resolution or rung count; the script's resolution check only enforces the source dimensions.

Calculate the storage budget

Your complete submission must satisfy:

submission bytes < 1.5 × supplied source file bytes

Count all submitted files: the manifest, initialization files, video segments, and shared audio segments. Use the original supplied MP4 as the reference; converting it first changes the budget and is not the intended rule.

For 120 seconds, estimate the size before encoding:

estimated bytes = (sum of video bitrates + 128) × 1000 × 120 / 8

Bitrates in this formula are in kbit/s. The 128 is one shared AAC audio track, counted once for the whole ladder. The division by eight converts bits to bytes. For the three-rung example above:

(400 + 1200 + 3000 + 128) × 1000 × 120 / 8 = 70,920,000 bytes

This estimate excludes packaging overhead and encoding variation. Before encoding, the script requires the estimate to be below 97% of the size limit to leave some headroom. After encoding, it measures the actual package and reports whether the strict 1.5× rule passes. The estimate is not a guarantee. If the final check fails, reduce your bitrate allocation and try a new output directory.

Encoding time and output

The script prints the validated ladder and estimated size, then runs two-pass H.264 encoding for every rung. Higher resolutions and more rungs take longer. All rungs use aligned two-second keyframes for DASH switching, with a single shared AAC audio track.

Optional controls apply to the whole ladder:

Option Default Meaning
--preset fast x264 speed/compression tradeoff; e.g. slow takes more encoding time
--threads 4 Positive number of encoder threads

The output directory contains manifest.mpd and fragmented MP4 initialization and media files. Temporary intermediate encodings are removed automatically. Upload the entire output directory's contents, preserving filenames, to the DASH server using the lab's transfer procedure. Restart the server node to publish the upload, then open its manifest URL in the DASH client.

Record your rung settings alongside the playback metrics so you can explain how your choices affected quality and buffering. A passing size check confirms the storage constraint; you still need to evaluate playback in the lab network.

Upload an encoded stream

Wait until encoding finishes and the size check passes. Leave the DASH server container running, then run this on the GNS3 VM:

/home/ttm4250/upload-stream.sh

Enter the server container ID or name (from docker ps) and the output directory you gave to the encoding script. You can also pass both directly:

/home/ttm4250/upload-stream.sh SERVER_CONTAINER ~/videos/student-submission

This uploads the whole directory into /srv/uploads/student-submission, keeping its name and all manifest/segment filenames. Relative paths work too: if the encoder wrote student-submission in your current directory, enter that same name. A trailing slash is fine. The upload source is on the VM where you run the helper, not on your laptop.

The directory must contain manifest.mpd. Its name must start with a letter or digit and contain only letters, digits, dots, underscores, or hyphens so it can be used in the stream URL. Parent directories may contain spaces; quote paths when passing them as arguments. Symlinks and special files are rejected.

An existing upload with the same name is rejected to avoid mixing different encodings. Use a new output directory name for each encoding set. The helper does not restart the server automatically. After a successful upload, stop/start the server node in GNS3 to publish it, then play:

http://SERVER_LAB_IP/student-submission/manifest.mpd

Do not modify the source while uploading. If the transfer fails, inspect and resolve any partial upload in /srv/uploads before restarting the server.

Collect metrics

After all your trials and metric logging have finished, leave the DASH client container running. On the GNS3 VM, run:

/home/ttm4250/collect-metrics.sh

Enter the client container ID or name (find it with docker ps), then press Enter to accept ~/results as the destination, or type another directory on the VM. The script exports the complete /srv/metrics directory, containing all trial runs, into ~/results/metrics/. Existing trial filenames and subdirectories are preserved so you can compare the collected results afterwards. There is no need to export each trial separately.

You can also supply the values directly:

/home/ttm4250/collect-metrics.sh CLIENT_CONTAINER ~/results

The script creates the destination if needed and refuses an existing metrics entry there, so an earlier complete export is not overwritten. To export again after more trials, move the previous export or choose another destination. Paths with spaces work; quote them when passed as command-line arguments. The copy uses docker exec and a tar stream rather than docker cp; source logs are retained. Your VM account needs Docker access. If a copy fails, inspect the error and use a fresh destination after resolving it; a failed copy may leave partial files.

Compare all test runs

Enter a meaningful test label in the DASH client before starting each run, for example ladder-A-load-profile-1. After exporting the logs, run on the VM:

python3 /home/ttm4250/analyze-runs.py ~/results

This creates ~/results/comparison.csv. Open it in a spreadsheet or load it into your analysis software. The script searches recursively, so the default export layout results/metrics/RUN_ID/ works directly. You can also point it at ~/results/metrics or an individual run directory.

To choose an output filename:

python3 /home/ttm4250/analyze-runs.py ~/results -o ~/comparison-ladder-A.csv

The output file must not already exist. Raw logs are never modified. Python 3 is the only requirement; running this script does not encode or play video.

There is one row per run, sorted by test_label and start time. Runs with the same label remain separate, identified by run_id and run_directory, so repetitions are not silently averaged. The label is taken from the client's saved metadata, not inferred from the stream filename.

CSV field Meaning
test_label, run_id, started_utc Experiment label and identity/time of this repetition
status, reason Whether playback ended, was saved early, or lacks a usable final summary
startup_delay_s Time until playback first began
elapsed_s Total wall-clock time measured by the player for the run
playback_wall_s Wall-clock time spent playing, excluding startup and stalls
final_playback_s Final position in the video; use to check comparable playback lengths
rebuffer_count, rebuffer_s Number and total duration of stalls after playback began
rebuffer_ratio Stall time / (playing time + stall time); excludes startup
rendered_quality_switches Changes in rendered representation, excluding the first selection
mean_video_bitrate_kbps Playing-time-weighted manifest bitrate of observed rendered representations
bitrate_observed_playback_s, bitrate_coverage_ratio How much playing time has bitrate observations, in seconds and as a fraction of playing time
dropped_frames, total_frames, dropped_frame_ratio Browser frame counts and dropped / total frames
mpd_url, abr_mode, dashjs_version Stream and client configuration identifiers
run_directory, notes Source log location and any missing/corrupt data warnings

Ratios are fractions, not percentages: 0.05 means 5%. Missing or undefined measurements are blank, not zero. Startup delay, stall accounting, switches, and weighted bitrate come from the client's summary.json; the analysis adds frame-drop and bitrate-coverage ratios. It does not approximate missing final measurements from samples or batches.

status=finished means the client recorded reason=ended. saved_partial includes manually stopped runs and initialization errors; inspect reason. incomplete means no final summary was saved; invalid means metadata or a summary could not be read. These runs remain visible in the CSV, with notes. Check status and final playback position before comparing performance.

Higher average bitrate is not automatically higher perceptual quality when resolution, FPS, or encoding choices differ. Evaluate bitrate together with stalling, startup delay, dropped frames, and visual quality. This table does not assign an arbitrary combined score. The current logs do not contain enough information to report a reliable time-weighted FPS or perceptual quality score.