In this fairly technical part, I’d like to break down the interactive movie engine itself and discuss some of the technical challenges I faced while building it.
Designing the Interactive Video Player
I loved the idea from the very beginning.
Building a standalone interactive video player within the technical limits of the time—something that genuinely combined a movie and a game—was an enormous and exciting challenge I didn’t want to miss.
The video player follows a playlist of interconnected video clips until the story reaches an ending. During certain clips, the player is presented with a time-limited set of choices, displayed together with a countdown timer. Based on the selected option—or the lack of one—the video player crossfades into the next clip. That transition may be immediate or delayed, but it always needs to feel smooth.
The following diagram explains the core idea:
| |===== Choices Visible ====|--|
| ^ showChoices ^ hideChoices
| [ crossfade ]
|---------------- Clip 1 --------------|-----|
| |------ Clip 2 ------>
cut cut (choice made)Each choice also affects internal variables, which in turn influence future options and enable different endings.
On paper, these were fairly clear product requirements. Every software engineer knows that initial sense of clarity and optimism—right up until implementation begins, and real constraints of technology and user experience start pushing back.
Tech Stack
At first, we considered hosting all clips on YouTube and using embedded video links to navigate between them. That approach failed almost immediately. The experience was fragmented, immersion was effectively zero, and any form of variable logic or deeper interaction was impossible.
It had to run on its own engine!
When evaluating technologies that could run smoothly on the web and still allow future porting to mobile platforms, Adobe Flash was the obvious choice for me at the time. I had already worked with Flash; it integrated well with the web and supported mobile deployment. HTML5 video APIs, on the other hand, were still immature at the time (YouTube officially switched its default player from Flash to HTML5 in January 20151).
Despite many shortcomings, prototyping complex interactions in Flash was fast and predictable.
The rest of the stack included HTML, CSS, JavaScript & JQuery, PHP & Nette, MySQL & Dibi, JSON & XML.
I initially used SVN for version control before switching to Git in 2023.
Movie Quality
It was 2011. Computers were relatively slow, and mobile devices were just waking up to the world of high-quality media. The first affordable consumer 4K displays appeared in late 2012, and 1080p was still considered high-end.
Keeping the movie at its original full HD resolution, 60 FPS, and zero compression would have resulted in over 20 GB of data to download into the browser cache. With the target market average internet connection speed of ~15 Mbps in 20122, even segmented downloads would take forever.
That was unacceptable. Every scene had to finish downloading before the previous one ended, allowing assets to load quietly in the background during playback.
The final video format was somewhere “in the middle”:
Container: Flash Video (FLV)
Codec: H.264 (AVC 1), 960×540 @ 25 FPS
Audio: AAC Stereo, 48 kHz
This reduced the entire movie to under 200 MB. The compression ratio was determined by the need to minimize loading interruptions while maintaining good quality. To support this, the movie was split into scenes of varying sizes that could be downloaded incrementally and in advance.
Building the Engine
You can’t just let AI vibe-code an application like this. Not in 2011. Even today, that would not work out of the box without tailoring it to the video content.
There was a shared core logic, but a large portion of the code had to be custom-tailored to the movie itself—its pacing, timing, and imperfections. At the time, there were no AI tools to enhance or even generate footage. Everything was built manually, through iteration after iteration, trial and error.
I strongly believe in iterative development. Throughout my professional career, I’ve consistently pushed my teams toward this mindset: start small, but make sure every iteration ends with a working system.
I jumped straight into prototyping based on early mockups and discussions.
I built it on a clean, modular mediator pattern with the main Controller class that coordinates all the specialized subsystems. The heart of the interactivity is the onFrame method: a hook triggered on every rendered movie frame. By anchoring all logic to the video framerate, the system treated the film itself as the primary clock, ticking every 40 milliseconds. I experimented with using time as the clock mechanism, but even with a small lag, the entire experience was ruined.
Matouš and Víťa recorded a few experimental shots with a friend, who served as the early model for Dana. Using just five clips, I built the first functional prototype and verified that sequential playback worked as intended.
At that point, we knew the concept was viable.
In the next iteration—still without final footage—I added branching choices. The prototype was crude, but functional. Subsequent iterations introduced smooth transitions, background music, multiple-choice mechanisms, and the use of the script.

This was not a system that could be designed top-down. On paper, everything looked reasonable. In practice, every prototype revealed a new way the illusion could break—an awkward cut, a mistimed choice, a transition that felt mechanical instead of cinematic. Each iteration wasn’t only about adding features, but also about identifying where the technology became visible and then removing that seam again.
In total, the original version of Interande took 46 iterations (commits) to build and launch. The 2023 version has taken 151 iterations so far.
The Script
From a technical perspective, the script wasn’t a screenplay—it was a data structure. A declarative description of timing, conditions, and transitions implemented as a JSON tree with the following structure:
...
v28a: {next: "v32"},
v28b: {
next: "v29b",
showChoices: 170,
hideChoices: 290,
points: 1, time: 2,
choices: [
{jump: "v29b", x: 250, y: 450, text: "HANA", insta: true},
{jump: "v29c", x: 430, y: 450, text: "JANA", insta: true},
{jump: "v29a", x: 600, y: 450, text: "DANA", insta: true},
]
},
...
v37x: {
next: "=time>4?v37y:v37n",
music: {
file: "ticho.mp3",
volume: 0.5
}
},
...What mattered was not what the characters said, but when something could be said, interrupted, or redirected without tearing the experience apart. The script defined intent; the engine had to survive reality.
Semantic Preloading
Bandwidth quickly became a narrative constraint. A loading screen isn’t just a technical artifact—it’s a visible rupture in immersion.
Although the application includes loading screens, careful optimization ensured they rarely appeared in practice.
The content is split into 16 parts, resulting in 15 potential internal loading moments corresponding to different scenes (bar, city, encounters, and so on). In the original version, scenes were loaded serially. The latest version loads scenes in two parallel queues until everything is fully downloaded—typically before the first scene even ends.
Loading as a Narrative Failure
I also implemented a metric to track the frequency of loading screens during gameplay. Below are statistics showing how many loadings players encountered per play between 2012 and 2025. They illustrate how average connection speeds improved over time, eventually becoming fast enough that downloads completed ahead of playback for 97 % of players by the end of 2025.
The initial peak reflects early load-distribution issues during peak demand. The long-term trend toward uninterrupted playback is visible even though neither the content nor the servers changed. The gap between 2021 and spring 2023 corresponds to the period when the movie was offline.
Since 2023, parallel loading has had only a marginal impact—modern connections are simply fast enough.
Infrastructure as Storytelling
What happens when the audience arrives all at once?
Initially, video files were served from a folder on my personal student hosting plan, which typically handled about 20 MB of traffic per day. After launch—and several times afterward—we peaked at over 230 GB per hour. This triggered alerts on the provider’s side and prompted them to contact me to resolve the situation and move us to a paid plan:
Sun, Jun 14, 2015, 10:00 PM
Dear Mr. Brnka,
During today, data traffic on interande.com increased to 160MB/s (1280Mbps). We are reaching out to resolve this situation and discuss pricing options, as this throughput highly exceeds your plan's SLA.
anafra.cz
While we expected high demand, we never imagined peaks of over 1,000 concurrent players, each streaming 200+ MB of content, sustained for several days. I had to implement a load balancer immediately while the rest of the team scrambled to secure additional storage backends.
We managed to get storage on the Czech University of Technology backbone for a while, and since 2015, the primary storage has been a web RAM disk provided by anafra.cz in exchange for a link on the web. Additional locations mainly serve as backups. This saved Interande, as the average daily data throughput matched a typical monthly quota of regular internet providers.
The load-balancing logic itself uses weighted distribution to prioritize faster storage while spreading traffic evenly. The downloader also implements retries: if a fragment isn’t available on one host, it automatically falls back to another.
The Narrative Angle
What does indecision mean in a story that keeps moving?
A central narrative decision was how the system should behave in the absence of player input. Traditional interactive systems block progression until a choice is made. This approach was incompatible with the cinematic continuity and immersion we wanted to achieve.
Instead, every decision point defined a default jump executed when the choice timer expired. From a technical perspective, this ensured deterministic progression. From a narrative perspective, it prevented the story from acknowledging the player’s hesitation.
The diagram of a “no choice” flow appears as follows:
| |== Choices Visible ==|
| [ crossfade ]
|---------------- Clip 1---------------|------ Clip 2 ------>
cut cut (no choice made = default jump)Even pausing is disabled during choice windows to prevent the story from acknowledging hesitation.
This mechanism maintains the temporal flow and prevents breaking immersion. Missed choices become narrative events, but the player is slightly penalized for indecision, as the “default path” leads to the worst ending.
Time became an active force rather than a passive resource. If you don’t decide what drink you want, the bartender decides for you!
Illusion of Seamless Editing

This was one of the most challenging parts of the implementation. Each clip began and ended with characters, objects, lighting, and camera framing in slightly different positions. At the same time, player choices could interrupt a clip at any moment, meaning transitions had to work from arbitrary cut points—while still appearing smooth.
The challenge was to programmatically cut the movie in real time and crossfade quickly enough to hide discontinuities. The problem becomes most apparent when a choice window appears near the end of a clip, leaving too little time for a clean crossfade once the decision is made. This caused a few compromises, especially in the dancing scene.
We chose a white transition for player-initiated choices and applied cubic easing to avoid linear, mechanical fades. In the latest version, crossfades are color-coded: black for default transitions, white to confirm that a player’s choice was registered.
Cinematic Karma
Players earn points by guiding the main character through dialogues and challenges, testing reflexes, memory, and decision-making—often without explicit hints. These points unlock or block narrative paths and determine endings.
The engine tracks two variables throughout the game and evaluates branching rules using a simple lexical analyzer driven by the following regular expression:
const regex = /^=(\w+)([><])(\d+)\?([a-zA-Z0-9-]+):([a-zA-Z0-9-]+)$/;So, for a jump definition of “if you have more than 7 points after this node, jump to v21; otherwise jump to v20” You’d go with:
v19: {next: "=points>7?v20-a:v20-b"}If you have 7 or fewer points, Dana walks away, and the game ends early.
Music and Sound Player

Flash’s audio capabilities were very limited, so I created a custom sound library to mix video audio with background music and adjust stereo balance. Music tracks were preloaded with each scene and played on loop.
The sound engine supported logarithmic, V-shaped crossfades with configurable duration. A linear fade sounds wrong to the human ear; a trigonometric curve sounds more cinematic.
A constant-power crossfade implementation looks like this:
const p = Math.min((now - crossFadeStart) / duration, 1);
outTrack.volume = Math.cos(p * 0.5 * Math.PI);
inTrack.volume = Math.sin(p * 0.5 * Math.PI);
if (p < 1) {
requestAnimationFrame(update);
} else {
outTrack.pause();
outTrack.volume = 0;
}The Limits of Portability
The original architecture accounted for mobile platforms from the start. Shortly after launch, I borrowed an Apple iPad and an Asus Transformer Android tablet and began experimenting with native ports.

Porting a Flash ActionScript project to Android and iOS turned out to be far more difficult than anticipated—and not merely because of tooling. Each platform imposed constraints that directly conflicted with the experience we were trying to preserve. Even basic assumptions, such as consistent resolution, aspect ratio, and video rendering behavior, broke down across devices.
Android
On Android, the most immediate limitation was the 50 MB application size cap. This made it impossible to bundle video assets directly into the APK. Instead, the app had to rely on downloading a separate OBB file containing the movie data.
In theory, this mirrored the web model. In practice, mobile connectivity at the time was significantly less reliable. Loading interruptions became frequent enough to disrupt the cinematic flow. To compensate, the entire asset bundle would have to be downloaded upfront and stored locally on the device before the first play. That would take a long time and most likely several attempts due to interruptions and retries.
While this approach was technically feasible, new issues emerged. During testing, I began observing intermittent blackouts during video playback—sudden failures that halted progress entirely. These issues were difficult to reproduce and even more challenging to eliminate reliably across devices.
iOS
iOS presented a different set of problems. Distribution constraints were less severe, but the platform struggled with seamless video transitions. Cuts that were effectively invisible on the web became noticeably abrupt on iOS, breaking immersion at precisely the moments where continuity mattered most.
This wasn’t a cosmetic flaw—it undermined one of the core design principles of the entire project. Smooth transitions were foundational. Without them, the experience no longer behaved like a film.
Why the Web Won
At that point, it became clear that these were not issues that could be solved through incremental fixes. They were fundamental mismatches between platform behavior and narrative requirements.
Rather than ship a compromised version of the experience, we decided to step back from native mobile apps and focus on the web, where the system behaved predictably, and the experience could remain intact.
At the time, expectations were high. The mobile version was meant to launch globally with an English voiceover and serve as a paid product that would help fund future work. That never happened.
More than a decade later, I finally restored the forgotten English assets in the 2023 web release—closing a long-postponed loop and offering Interande to a worldwide audience.
The Web Cinema
The web version of Interande runs on the Nette backend, which handles session, navigation, language support, scoring, and telemetry. The server runs three presenters and two data models.
Every completed playthrough is stored in a database, allowing long-term analysis of player behavior and system performance.
From the beginning, the goal was to keep the experience lightweight. The application needed to load quickly, run reliably, and stay out of the way of the story. There was no interest in visual excess or interface novelty—only in preserving immersion.
During the design phase, Matouš and I settled on a restrained visual language: a dark theme, subtle patterns, a limited color palette, and gentle blur effects. The interface was designed to feel present when needed and invisible otherwise.
The web itself was deliberately cinematic. In the latest version, if the mouse cursor remains inside the video player window, the interface fades out entirely, leaving only the title visible. Controls reappear on pause or when the cursor leaves the video player area—a simple interaction model that reinforces focus on the film rather than the application.
Some Challenges Persist
Safari, in particular, remains problematic due to its restrictions on autoplaying audio and video. Requiring explicit user interaction for every playback transition undermines the very continuity the system is designed to protect. These limitations are outside the application’s control, but they highlight how fragile immersive experiences can be when platform policies intervene.
Despite this, the web remains the most natural home for Interande. It offers reach, flexibility, and—crucially—the ability to evolve. Unlike native platforms, the web version can adapt incrementally without forcing the story into a different shape.
I still have one major task remaining in the TODO list: implementing the correct mobile styling.
Playing It Again and Again
There were no unit tests. There was no automated test suite. There wasn’t even a meaningful way to simulate the experience without actually playing it.
Internal Instrumentation
For my own dev testing, I used a simple debug panel showing mouse coords for proper choice buttons positioning, variable counters, time elapsed, current and next default jump, frame number/total, autochoice switch, language, memory footprint, and a simple progress bar:
This panel made the invisible visible, allowing me to reason about timing and state without breaking immersion during regular play.
Also, a lot of logging like this:
[net] downlink= 10 parallelDownloads= 2
[preload] preloadPack: Uvod media: 7
[preload] preloadPack: Bar media: 15
[telemetry] game started
[xfade] crossfadeTo: - => v0
[ui] togglePlayPauseButton: ALLOWED
[music] Switching soundtrack to 0.mp3 vol= 0.5
[xfade] smoothFadeOut enabled
[preload] preloadPack: Sedacky media: 9
[playback] pause()
[focus] User switched away
...helped me to do most of the heavy lifting. Especially, the autoChoice functionality is priceless. When turned on, it emulates random choices instantly or with a slight delay, allowing you to finish the entire movie in a few seconds to see whether everything calculates correctly, etc.
During the 2012 crunch, we didn’t have this functionality, and the entire team played Interande too many times to stay sane.
Based on the logs, I’ve personally played it over 1000 times!
Testing in the Wild
For external testing, I used my own web-based testing system, which I previously developed to test Flash games. It supported access control, versioning, discussions, SWF instrumentation, and invite keys. It was called FleeBee.
In this system, a small circle of trusted “VIP” testers—our friends, schoolmates, and crew members—helped tirelessly. They took different paths, tried to break the flow, intentionally hesitated, clicked too early or too late, refreshed at the wrong moment, and reported anything that felt off. Logs, screenshots, and long messages describing how something felt were far more valuable than stack traces.
Timing, perception, and continuity only revealed themselves through real playback. A technically correct transition could still feel wrong. A choice that appeared on time could still feel late. The illusion either held—or it didn’t.
Every reported issue fell into one of two categories:
The system broke (a bug, crash, or missing asset)
The illusion broke (a bad cut, awkward pause, or scene continuity issue)
The second category was more difficult to fix. The main limitation was that we couldn’t redo the scenes; the material was recorded, and we could only edit what we had. That’s where the script supervisor turned out to be critical—one forgotten purse in the present and then missing in two adjacent shots, and there is no way to cover this up programmatically!
Fixes were validated the same way they were discovered: by playing again. Many times. Often late at night. Usually, after thinking that something was already “done.” Each new build closed some seams and exposed others.
In hindsight, this form of testing more closely resembled rehearsals than verification.
Tracing the Story
From the start, Interande collects telemetry—not for monetization or optimization loops, but to understand how the experience behaves in the real world.
For each playthrough, the system records (besides Google Analytics):
IP address (for coarse geographic and infrastructure insights, also to roughly distinguish unique players)
Language and game end timestamp
Score and final values of internal variables
Path taken through the story
Number of loading screens encountered
These metrics serve two purposes.
The first is technical validation. Load counts revealed whether preloading strategies worked as intended. Path distributions exposed edge cases that weren’t sufficiently exercised during testing. Sudden spikes in failed or incomplete sessions often pointed directly to infrastructure or hosting issues.
The second is narrative insight. Seeing how players actually move through the story—where they hesitate, which paths are rare, which endings are common—offers a reality check against assumptions made during design. Some branches that felt important during writing turned out to be rarely reached. Others emerged as dominant simply because they aligned better with player behavior under time pressure.
Over time, the data also became an unintended historical record. Changes in average loading counts mirrored improvements in global connectivity. Infrastructure upgrades showed up as smoother playthroughs, with no changes to a single line of narrative code.
In that sense, the metrics didn’t just describe the game. They documented the environment in which the story lived—and how that environment and player behavior slowly evolved around it.
Final Thoughts
Looking back, I realize I wasn’t just building a video player. I was building a system to manage human attention. Every technical decision—weighted load balancing, frame-accurate timing, smooth crossfades, invisible defaults—served the same purpose: to make the technology disappear.
Immersion in interactive film is fragile. A single loading screen, a cut too visible, or a sound transition that feels mechanical is enough to remind the audience that they are inside an application. Much of the work described here exists solely to prevent that moment—to ensure the story didn’t pause, buffer, or wait, but simply continued.
What surprised me most in retrospect is how closely the technical architecture eventually mirrored the narrative intent. The engine powers more than just a video player. Time moves forward. Indecision has consequences. Continuity matters more than control.
In interactive film, the highest praise a developer can get is for the audience to forget there’s any code—to feel that the story didn’t load, but simply unfolded.
In the next and final article of the series, I’ll share the story of the launch, the shutdown after Flash’s death, and the unexpected rebirth—along with a few bonuses.
Stay tuned, and thanks for reading!





















