Halo Infinite Film Reverse Engineering

Halo Infinite enables players to watch a film post-match. The film, in this context, is not your typical MP4 file. Instead, it represents metadata that is passed directly to the game engine, where it’s being processed and every player’s position, performance, medals, and other information is passed through a set of undocumented binary pieces rather than in typical “View from the point of view of one player” pre-compiled content.

Because the file is clearly not a video file, I am making the following assumptions:

  • All metadata for the film is actually just that, metadata. There is no media embedded.
  • The film playback is flexible enough that it allows moving the camera around the map, zooming in and out - it’s likely that the information is mostly “tagging” what players are doing through the in-game asset, which includes:
    • Kills
    • Deaths
    • Medals
    • Type of weapon used
    • Armor configuration
    • Position on the map
    • Timestamps for every change in position/action
    • Likely other items

Major events, like kills, deaths, and medals, are represented on the timeline at the bottom of the game screen:

Footer for the Halo Infinite film visualization

The timeline is broken down into individual chunks, usually (from my observation) around 24 or 26. You can see them if you hover over them.

Footer for the Halo Infinite film visualization

Specifically, they are all delineated within the timeline itself in the game UI:

Footer for the Halo Infinite film visualization

So, to analyze this further, I used Fiddler to track outbound requests when I try to watch a film. For the purpose of this analysis, I picked two matches:

Match ID Match Type Outcome Tracked Gamertag Map
031f73c4-6151-4674-b313-e2b531d2e181 Fiesta Win BreadKrtek Aquarius
12499037-baf8-4b3d-99fe-605419c57850 Fiesta Loss BreadKrtek Catalyst

Just for posterity, and to make sure that I am able to track the stats, the scoreboards for each match is also below.

Scoreboard in Halo Infinite showing stats for a winning Fiesta match

And for the losing match, respectively:

Scoreboard in Halo Infinite showing stats for a losing Fiesta match

Both match films can be obtained through a request to the following endpoints:

https://discovery-infiniteugc.svc.halowaypoint.com
	/hi
	/films
	/matches
	/031f73c4-6151-4674-b313-e2b531d2e181
	/spectate

And, respectively:

https://discovery-infiniteugc.svc.halowaypoint.com
	/hi
	/films
	/matches
	/12499037-baf8-4b3d-99fe-605419c57850
	/spectate

API Responses

For each of the requests above, I get JSON data containing locations of each independent film chunk.

031f73c4-6151-4674-b313-e2b531d2e181 - Fiesta (Win)

{
    "FilmStatusBond": 1,
    "CustomData": {
        "FilmLength": 480428,
        "Chunks": [
            {
                "Index": 0,
                "ChunkStartTimeOffsetMilliseconds": 0,
                "DurationMilliseconds": 62,
                "ChunkSize": 348933,
                "FileRelativePath": "/filmChunk0",
                "ChunkType": 1
            },
            {
                "Index": 1,
                "ChunkStartTimeOffsetMilliseconds": 0,
                "DurationMilliseconds": 19997,
                "ChunkSize": 72201,
                "FileRelativePath": "/filmChunk1",
                "ChunkType": 2
            },
            {
                "Index": 2,
                "ChunkStartTimeOffsetMilliseconds": 19997,
                "DurationMilliseconds": 20005,
                "ChunkSize": 128378,
                "FileRelativePath": "/filmChunk2",
                "ChunkType": 2
            },
            {
                "Index": 3,
                "ChunkStartTimeOffsetMilliseconds": 40003,
                "DurationMilliseconds": 20003,
                "ChunkSize": 306379,
                "FileRelativePath": "/filmChunk3",
                "ChunkType": 2
            },
            {
                "Index": 4,
                "ChunkStartTimeOffsetMilliseconds": 60007,
                "DurationMilliseconds": 20002,
                "ChunkSize": 297136,
                "FileRelativePath": "/filmChunk4",
                "ChunkType": 2
            },
            {
                "Index": 5,
                "ChunkStartTimeOffsetMilliseconds": 80010,
                "DurationMilliseconds": 20003,
                "ChunkSize": 316367,
                "FileRelativePath": "/filmChunk5",
                "ChunkType": 2
            },
            {
                "Index": 6,
                "ChunkStartTimeOffsetMilliseconds": 100013,
                "DurationMilliseconds": 20003,
                "ChunkSize": 306137,
                "FileRelativePath": "/filmChunk6",
                "ChunkType": 2
            },
            {
                "Index": 7,
                "ChunkStartTimeOffsetMilliseconds": 120017,
                "DurationMilliseconds": 20003,
                "ChunkSize": 293680,
                "FileRelativePath": "/filmChunk7",
                "ChunkType": 2
            },
            {
                "Index": 8,
                "ChunkStartTimeOffsetMilliseconds": 140021,
                "DurationMilliseconds": 20002,
                "ChunkSize": 276608,
                "FileRelativePath": "/filmChunk8",
                "ChunkType": 2
            },
            {
                "Index": 9,
                "ChunkStartTimeOffsetMilliseconds": 160024,
                "DurationMilliseconds": 20003,
                "ChunkSize": 283073,
                "FileRelativePath": "/filmChunk9",
                "ChunkType": 2
            },
            {
                "Index": 10,
                "ChunkStartTimeOffsetMilliseconds": 180027,
                "DurationMilliseconds": 20004,
                "ChunkSize": 268748,
                "FileRelativePath": "/filmChunk10",
                "ChunkType": 2
            },
            {
                "Index": 11,
                "ChunkStartTimeOffsetMilliseconds": 200031,
                "DurationMilliseconds": 20003,
                "ChunkSize": 291298,
                "FileRelativePath": "/filmChunk11",
                "ChunkType": 2
            },
            {
                "Index": 12,
                "ChunkStartTimeOffsetMilliseconds": 220035,
                "DurationMilliseconds": 20003,
                "ChunkSize": 255427,
                "FileRelativePath": "/filmChunk12",
                "ChunkType": 2
            },
            {
                "Index": 13,
                "ChunkStartTimeOffsetMilliseconds": 240038,
                "DurationMilliseconds": 20003,
                "ChunkSize": 280775,
                "FileRelativePath": "/filmChunk13",
                "ChunkType": 2
            },
            {
                "Index": 14,
                "ChunkStartTimeOffsetMilliseconds": 260042,
                "DurationMilliseconds": 20003,
                "ChunkSize": 271552,
                "FileRelativePath": "/filmChunk14",
                "ChunkType": 2
            },
            {
                "Index": 15,
                "ChunkStartTimeOffsetMilliseconds": 280045,
                "DurationMilliseconds": 20003,
                "ChunkSize": 275109,
                "FileRelativePath": "/filmChunk15",
                "ChunkType": 2
            },
            {
                "Index": 16,
                "ChunkStartTimeOffsetMilliseconds": 300049,
                "DurationMilliseconds": 20003,
                "ChunkSize": 280795,
                "FileRelativePath": "/filmChunk16",
                "ChunkType": 2
            },
            {
                "Index": 17,
                "ChunkStartTimeOffsetMilliseconds": 320053,
                "DurationMilliseconds": 20003,
                "ChunkSize": 267615,
                "FileRelativePath": "/filmChunk17",
                "ChunkType": 2
            },
            {
                "Index": 18,
                "ChunkStartTimeOffsetMilliseconds": 340057,
                "DurationMilliseconds": 20002,
                "ChunkSize": 295718,
                "FileRelativePath": "/filmChunk18",
                "ChunkType": 2
            },
            {
                "Index": 19,
                "ChunkStartTimeOffsetMilliseconds": 360060,
                "DurationMilliseconds": 20003,
                "ChunkSize": 297000,
                "FileRelativePath": "/filmChunk19",
                "ChunkType": 2
            },
            {
                "Index": 20,
                "ChunkStartTimeOffsetMilliseconds": 380064,
                "DurationMilliseconds": 20003,
                "ChunkSize": 298522,
                "FileRelativePath": "/filmChunk20",
                "ChunkType": 2
            },
            {
                "Index": 21,
                "ChunkStartTimeOffsetMilliseconds": 400068,
                "DurationMilliseconds": 20005,
                "ChunkSize": 344172,
                "FileRelativePath": "/filmChunk21",
                "ChunkType": 2
            },
            {
                "Index": 22,
                "ChunkStartTimeOffsetMilliseconds": 420074,
                "DurationMilliseconds": 20004,
                "ChunkSize": 302836,
                "FileRelativePath": "/filmChunk22",
                "ChunkType": 2
            },
            {
                "Index": 23,
                "ChunkStartTimeOffsetMilliseconds": 440079,
                "DurationMilliseconds": 20001,
                "ChunkSize": 279184,
                "FileRelativePath": "/filmChunk23",
                "ChunkType": 2
            },
            {
                "Index": 24,
                "ChunkStartTimeOffsetMilliseconds": 460081,
                "DurationMilliseconds": 20001,
                "ChunkSize": 223305,
                "FileRelativePath": "/filmChunk24",
                "ChunkType": 2
            },
            {
                "Index": 25,
                "ChunkStartTimeOffsetMilliseconds": 480082,
                "DurationMilliseconds": 299,
                "ChunkSize": 18639,
                "FileRelativePath": "/filmChunk25",
                "ChunkType": 2
            },
            {
                "Index": 26,
                "ChunkStartTimeOffsetMilliseconds": 480381,
                "DurationMilliseconds": 2,
                "ChunkSize": 89809,
                "FileRelativePath": "/filmChunk26",
                "ChunkType": 3
            }
        ],
        "HasGameEnded": true,
        "ManifestRefreshSeconds": 30,
        "MatchId": "031f73c4-6151-4674-b313-e2b531d2e181",
        "FilmMajorVersion": 24
    },
    "BlobStoragePathPrefix": "https://blobs-infiniteugc.svc.halowaypoint.com/ugcstorage/film/d22da931-8574-4fba-8631-65c481ef16bb/b6719b55-7df2-4e4c-a068-efe8c3578dfe/",
    "AssetId": "d22da931-8574-4fba-8631-65c481ef16bb"
}

12499037-baf8-4b3d-99fe-605419c57850 - Fiesta (Loss)

{
    "FilmStatusBond": 1,
    "CustomData": {
        "FilmLength": 505247,
        "Chunks": [
            {
                "Index": 0,
                "ChunkStartTimeOffsetMilliseconds": 0,
                "DurationMilliseconds": 5555,
                "ChunkSize": 349081,
                "FileRelativePath": "/filmChunk0",
                "ChunkType": 1
            },
            {
                "Index": 1,
                "ChunkStartTimeOffsetMilliseconds": 0,
                "DurationMilliseconds": 19998,
                "ChunkSize": 78720,
                "FileRelativePath": "/filmChunk1",
                "ChunkType": 2
            },
            {
                "Index": 2,
                "ChunkStartTimeOffsetMilliseconds": 19998,
                "DurationMilliseconds": 20004,
                "ChunkSize": 189362,
                "FileRelativePath": "/filmChunk2",
                "ChunkType": 2
            },
            {
                "Index": 3,
                "ChunkStartTimeOffsetMilliseconds": 40002,
                "DurationMilliseconds": 20005,
                "ChunkSize": 315125,
                "FileRelativePath": "/filmChunk3",
                "ChunkType": 2
            },
            {
                "Index": 4,
                "ChunkStartTimeOffsetMilliseconds": 60007,
                "DurationMilliseconds": 20013,
                "ChunkSize": 333841,
                "FileRelativePath": "/filmChunk4",
                "ChunkType": 2
            },
            {
                "Index": 5,
                "ChunkStartTimeOffsetMilliseconds": 80021,
                "DurationMilliseconds": 20013,
                "ChunkSize": 307749,
                "FileRelativePath": "/filmChunk5",
                "ChunkType": 2
            },
            {
                "Index": 6,
                "ChunkStartTimeOffsetMilliseconds": 100034,
                "DurationMilliseconds": 20003,
                "ChunkSize": 318636,
                "FileRelativePath": "/filmChunk6",
                "ChunkType": 2
            },
            {
                "Index": 7,
                "ChunkStartTimeOffsetMilliseconds": 120038,
                "DurationMilliseconds": 20002,
                "ChunkSize": 313126,
                "FileRelativePath": "/filmChunk7",
                "ChunkType": 2
            },
            {
                "Index": 8,
                "ChunkStartTimeOffsetMilliseconds": 140041,
                "DurationMilliseconds": 20003,
                "ChunkSize": 340362,
                "FileRelativePath": "/filmChunk8",
                "ChunkType": 2
            },
            {
                "Index": 9,
                "ChunkStartTimeOffsetMilliseconds": 160045,
                "DurationMilliseconds": 20002,
                "ChunkSize": 318879,
                "FileRelativePath": "/filmChunk9",
                "ChunkType": 2
            },
            {
                "Index": 10,
                "ChunkStartTimeOffsetMilliseconds": 180048,
                "DurationMilliseconds": 20003,
                "ChunkSize": 330013,
                "FileRelativePath": "/filmChunk10",
                "ChunkType": 2
            },
            {
                "Index": 11,
                "ChunkStartTimeOffsetMilliseconds": 200051,
                "DurationMilliseconds": 20004,
                "ChunkSize": 346136,
                "FileRelativePath": "/filmChunk11",
                "ChunkType": 2
            },
            {
                "Index": 12,
                "ChunkStartTimeOffsetMilliseconds": 220055,
                "DurationMilliseconds": 20004,
                "ChunkSize": 294575,
                "FileRelativePath": "/filmChunk12",
                "ChunkType": 2
            },
            {
                "Index": 13,
                "ChunkStartTimeOffsetMilliseconds": 240059,
                "DurationMilliseconds": 20003,
                "ChunkSize": 309791,
                "FileRelativePath": "/filmChunk13",
                "ChunkType": 2
            },
            {
                "Index": 14,
                "ChunkStartTimeOffsetMilliseconds": 260062,
                "DurationMilliseconds": 20004,
                "ChunkSize": 289045,
                "FileRelativePath": "/filmChunk14",
                "ChunkType": 2
            },
            {
                "Index": 15,
                "ChunkStartTimeOffsetMilliseconds": 280066,
                "DurationMilliseconds": 20004,
                "ChunkSize": 294496,
                "FileRelativePath": "/filmChunk15",
                "ChunkType": 2
            },
            {
                "Index": 16,
                "ChunkStartTimeOffsetMilliseconds": 300071,
                "DurationMilliseconds": 20002,
                "ChunkSize": 336322,
                "FileRelativePath": "/filmChunk16",
                "ChunkType": 2
            },
            {
                "Index": 17,
                "ChunkStartTimeOffsetMilliseconds": 320074,
                "DurationMilliseconds": 20003,
                "ChunkSize": 277272,
                "FileRelativePath": "/filmChunk17",
                "ChunkType": 2
            },
            {
                "Index": 18,
                "ChunkStartTimeOffsetMilliseconds": 340078,
                "DurationMilliseconds": 20002,
                "ChunkSize": 305708,
                "FileRelativePath": "/filmChunk18",
                "ChunkType": 2
            },
            {
                "Index": 19,
                "ChunkStartTimeOffsetMilliseconds": 360081,
                "DurationMilliseconds": 20003,
                "ChunkSize": 316180,
                "FileRelativePath": "/filmChunk19",
                "ChunkType": 2
            },
            {
                "Index": 20,
                "ChunkStartTimeOffsetMilliseconds": 380085,
                "DurationMilliseconds": 20003,
                "ChunkSize": 319477,
                "FileRelativePath": "/filmChunk20",
                "ChunkType": 2
            },
            {
                "Index": 21,
                "ChunkStartTimeOffsetMilliseconds": 400089,
                "DurationMilliseconds": 20003,
                "ChunkSize": 329350,
                "FileRelativePath": "/filmChunk21",
                "ChunkType": 2
            },
            {
                "Index": 22,
                "ChunkStartTimeOffsetMilliseconds": 420092,
                "DurationMilliseconds": 20003,
                "ChunkSize": 311572,
                "FileRelativePath": "/filmChunk22",
                "ChunkType": 2
            },
            {
                "Index": 23,
                "ChunkStartTimeOffsetMilliseconds": 440096,
                "DurationMilliseconds": 20004,
                "ChunkSize": 291693,
                "FileRelativePath": "/filmChunk23",
                "ChunkType": 2
            },
            {
                "Index": 24,
                "ChunkStartTimeOffsetMilliseconds": 460101,
                "DurationMilliseconds": 20002,
                "ChunkSize": 324191,
                "FileRelativePath": "/filmChunk24",
                "ChunkType": 2
            },
            {
                "Index": 25,
                "ChunkStartTimeOffsetMilliseconds": 480104,
                "DurationMilliseconds": 19600,
                "ChunkSize": 265578,
                "FileRelativePath": "/filmChunk25",
                "ChunkType": 2
            },
            {
                "Index": 26,
                "ChunkStartTimeOffsetMilliseconds": 499705,
                "DurationMilliseconds": 2,
                "ChunkSize": 115279,
                "FileRelativePath": "/filmChunk26",
                "ChunkType": 3
            }
        ],
        "HasGameEnded": true,
        "ManifestRefreshSeconds": 30,
        "MatchId": "12499037-baf8-4b3d-99fe-605419c57850",
        "FilmMajorVersion": 24
    },
    "BlobStoragePathPrefix": "https://blobs-infiniteugc.svc.halowaypoint.com/ugcstorage/film/9f091df0-3a70-412c-a285-efc7bf79f31e/e276e1de-946e-45a8-a363-2e48426ff257/",
    "AssetId": "9f091df0-3a70-412c-a285-efc7bf79f31e"
}

Observations - JSON Data

Some less obvious notes about the data above:

  • There are three different chunk types: 1, 2, and 3. Given their position, 1 is likely the “intro” chunk that contains some kind of starter information, 2 is likely a record of the gameplay, and 3 is likely some kind of outro/cleanup.
  • Each chunk has a millisecond offset and an associated duration.
  • FilmMajorVersion likely refers to some kind of file format/game processing engine version rather than a version of the film itself.
  • FilmStatusBond is likely the status of the film (e.g., processed or still processing). The Bond suffix is probably a red herring if we think that the data is bond-encoded, as it likely is just a representation of a type name that is leaking (i.e., someone forgot to name it in accordance with the rest of the guidelines).
    • This assumption stems from the fact that I’ve seen through the API error messages that quite a few types are using *Bond conventions.

Film chunks

As I called out in my earlier research, each film chunk is actually a zlib-packed file. This can be observed from the file header:

zlib file header in Visual Studio

Decompressing the chunks can be done with the help of this C# snippet, that relies on SharpZipLib:

using ICSharpCode.SharpZipLib.Zip.Compression;

namespace HaloFilmLoader
{
    internal class Program
    {
        static void Main(string[] args)
        {
            var urlBase = "https://blobs-infiniteugc.svc.halowaypoint.com/ugcstorage/film/9f091df0-3a70-412c-a285-efc7bf79f31e/e276e1de-946e-45a8-a363-2e48426ff257/";
            var lastChunkId = 26;
            var suffix = "filmChunk";

            for (int i = 0; i <= lastChunkId; i++)
            {
                Console.WriteLine($"Downloading chunk {i}...");
                var fullUrl = new Uri($"{urlBase}{suffix}{i}");

                Task.Run(async () =>
                {
                    HttpClient client = new();
                    var response = await client.GetAsync(fullUrl);

                    byte[] data = await response.Content.ReadAsByteArrayAsync();

                    Inflater decompressor = new Inflater();
                    decompressor.SetInput(data);

                    using (MemoryStream ms = new MemoryStream(data.Length))
                    {
                        byte[] buffer = new byte[1024];
                        while (!decompressor.IsFinished)
                        {
                            int count = decompressor.Inflate(buffer);
                            ms.Write(buffer, 0, count);
                        }

                        using (var fs = new FileStream($"{suffix}{i}", FileMode.Create))
                        {
                            ms.WriteTo(fs);
                        }
                    }
                }).GetAwaiter().GetResult();
            }
        }
    }
}

This will both download and decompress the chunks.

To compare the compressed and de-compressed files:

File explorer showing listing of compressed files

Compare this to the end result:

File explorer showing listing of uncompressed files

Some observations:

  1. Only the first and last chunks (“header” and “footer”) contain any references to gamertags.
  2. Each chunk type can be identified by the first two bytes:
    • 18 00 - header chunk
    • 01 00 - content chunk
    • 09 00 - footer chunk
  3. Bytes 4-7 in each chunk don’t seem to change, with the exception of the “footer” (last chunk).
    • 14 25 02 00
  4. Duration that is in the JSON response seems to match in-game duration of the chunk.
  5. In the last chunk, player gamertags seem to be preceded by 16 bytes of data that seems to be mostly unique. That is - the byte sequence is unique in the loss match “footer”, but in the win match footer the same sequence can be seen attributed to two different players.
  6. In the “header” file, at position C:3430h we have a time_t representation of the match time in UTC.
  7. The decompressed “header” seems to contain a lot of the game engine initialization parameters.
  8. In the same “header” file (first chunk), at position C:31C0h there is a string that represents the game build.

Let’s get back to the “footer” and gamertag listing there. For example, let’s say we look at the loss match, we see that in the “footer” the following players are mentioned, for each team respectively (gamertags shortened for posterity).

Team 1:

  • AG - 4 mentions (sequence 1)
  • Guppy - 3 mentions (sequence 3)
  • ROG - 3 mentions (sequence 8)
  • P0P - 6 mentions (sequence 7)

Team 2:

  • amc - 2 mentions (sequence 5)
  • Squirrely - 1 mention (sequence 6)
  • BreadKrtek - 2 mentions (sequence 4)
  • U2K - 1 mention (sequence 2)

Each of these players has a unique 16 byte sequence preceding every mention of their gamertag in the “footer” file. Per above, the sequences are as follows:

Sequence 1:

19 19 6D 56
00 00 00 00
ED 98 DB 05
45 52 6F 37

Sequence 2:

2E B2 E9 AD
00 00 00 00
0E DF 88 6D
D5 06 59 02

Sequence 3:

90 E6 68 E5
00 00 00 00
F5 47 22 31
4F 34 78 8F

Sequence 4:

7B F4 6C 0C
00 00 00 00
78 BF A7 67
D9 C0 91 22

Sequence 5:

57 61 40 B5
00 00 00 00
F2 A3 26 47
86 A0 E0 8A

Sequence 6:

2B 9B C1 FB
00 00 00 00
54 CD D8 AA
45 52 6F 37

Sequence 7:

3C 2E 9F D3
00 00 00 00
0E DF 88 6D
D5 06 59 02

Sequence 8:

F8 49 33 A5
00 00 00 00
F5 47 22 31
4F 34 78 8F

I initially hypothesized that this is some kind of GUID, but that seems unlikely because the first 8 bytes seem to contain a padding (notice the 00 00 00 00), and the same 8 bytes seem to re-appear in other “footer” files for other players, along with re-appearing in the same “footer” file but not near any player gamertags.

Updated observations (August 4, 2022)

Digging through the files a bit more, I realized that I probably bit off more than I should’ve at first. Instead of running real-life match analysis, I started with custom games, that seem to still produce films, but I can eliminate as much variation as possible. To start, I tried to see if there is any difference between two identical games (map, duration, configuration), and seems like there are changes in the first film chunk beyond just the match time. Specifically, there are bytes that are changed in the “secondary” fragment of the file, the part that follows the engine configuration pieces.

That said, some of the fragments that changed are repeating within the chunk, but are inconsistent in appearance and length of the following chunk. More analysis is warranted here. I am also thinking that potentially the first two bytes of the “secondary” fragment of the first film chunk represent the length of the following byte array, but that is not confirmed across different maps just yet.

Is this Bond-formatted? Doesn’t look like it, but you never know. Currently I tried parsing chunks through my Bond decoder but there are no good results for it. Maybe it’s compression of some kind?

I think my earlier observations around gamertags might be a red herring - they are too inconsistent to draw any concrete conclusions on yet.

Given that everything so far has been in Bond, I am assuming that there is a good chunk of data here that is also Bond-encoded since on the surface it looks like gibberish, and I don’t see any obvious compression headers.

Notes mentioning this note


Here are all the notes in this garden, along with their links, visualized as a graph.