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:
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:
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:
| 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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.