Last active 1629393043

Code to make your own Black Mirror: Bandersnatch

Revision fe181d8bac53c4bc2a516255f6c1917414a14a38

bandersnatch.md Raw

Bandersnatch Maker

This is a program I wrote on a live stream on the 14th (and 15th) of June 2019. https://www.twitch.tv/videos/438871676

You'll need some things.

You'll need to edit the two js files to remove bandersnatch= and SegmentMap= respectively so they parse as valid json. If you encounter an error about utf-8 bom use dos2unix to fix it. Rename the files to bandersnatch.json and SegmentMap.json.

Open a python interpreter in the same directory as SegmentMap.js. I'd probably recommend 3.7. Paste this in:

import json

with open("SegmentMap.json") as f:
  smap = json.load(f)

def msToTS(ms):
  s,ms = divmod(ms,1000)
  m,s = divmod(s,60)
  h,m = divmod(m,60)
  return "{:02d}:{:02d}:{:02d}.{:03d}".format(h,m,s,ms)

for segment in smap.values():
  ss = ""
  t = ""
  # working around ffmpeg seek to previous keyframe
  if segment["startTimeMs"] > 5000:
    # 40ms subtracted to account for one frame difference in 25fps source video
    ss = " -ss " + msToTS(segment["startTimeMs"]-4960)
  if "endTimeMs" in segment:
    t = " -t {}".format((segment["endTimeMs"]-segment["startTimeMs"])/1000)
  print("""ffmpeg{} -i bandersnatch.mkv -ss 5{} {}.mkv 2>/dev/null""".format(ss, t, segment))

You'll get a bunch of lines to run to generate clips from the full movie. I'd suggest putting these lines into a bash script and leaving it to run for a while in screen or tmux. This will take a very long time dependent on your CPU.

While that's going, generate some movies!

$ python bandersnatch.py > concat.txt
picked 1A
out=['1E']
out=['1D']
poss=['1E', '1D']
picked 1D
out=['1H']
out=['1G']
poss=['1H', '1G']
picked 1G
...
out=['8JA']
out=['8JB1']
poss=['8JA', '8JB1']
picked 8JA
out=['0Cr4', '0cr3']
out=['0Cr4', '0cr3']
poss=['0Cr4', '0cr3']
picked 0Cr4 and added IDNT
01:04:02.610

The stderr output displays what choices were possible at each stage, which segment it picked, and the total length of the movie.

The output file concat.txt is produced in ffmpeg concat format, render it in the same directory as the segment files (e.g IDNT.mkv) with:

ffmpeg -f concat -i concat.txt -c copy `date +%Y%m%d-%H%M%S.mkv`
bandersnatch.py Raw
1#!/usr/bin/env python3
2from __future__ import print_function
3from random import choice
4import json
5from sys import stderr
6
7with open("bandersnatch.json") as f:
8 bandersnatch = json.load(f)
9
10with open("SegmentMap.json") as f:
11 smap = json.load(f)
12
13initial_state = { "p_sp": True, "p_tt": True, "p_8a": False, "p_td": True, "p_cs": False, "p_w1": False, "p_2b": False, "p_3j": False, "p_pt": False, "p_cd": False, "p_cj": False, "p_sj": False, "p_sj2": False, "p_tud": False, "p_lsd": False, "p_vh": False, "p_3l": False, "p_3s": False, "p_3z": False, "p_ps": "n", "p_wb": False, "p_kd": False, "p_bo": False, "p_5v": False, "p_pc": "n", "p_sc": False, "p_ty": False, "p_cm": False, "p_pr": False, "p_3ad": False, "p_s3af": False, "p_nf": False, "p_np": False, "p_ne": False, "p_pp": False, "p_tp": False, "p_bup": False, "p_be": False, "p_pe": False, "p_pae": False, "p_te": False, "p_snt": False, "p_8j": False, "p_8d": False, "p_8m": False, "p_8q": False, "p_8s": False, "p_8v": False, "p_vs": "n", "p_scs": False, "p_3ab": False, "p_3ac": False, "p_3aj": False, "p_3ah": False, "p_3ak": False, "p_3al": False, "p_3af": False, "p_5h": False, "p_5ac": False, "p_5ag": False, "p_5ad": False, "p_6c": False, "length": 0 }
14state = dict(initial_state)
15
16moments = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["momentsBySegment"]
17preconditions = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["preconditions"]
18segmentGroups = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["segmentGroups"]
19
20def msToTS(ms):
21 s,ms = divmod(ms,1000)
22 m,s = divmod(s,60)
23 h,m = divmod(m,60)
24 return "{:02d}:{:02d}:{:02d}.{:03d}".format(h,m,s,ms)
25
26def conditionHandler(cond):
27 global state
28 if not cond:
29 return True
30 if cond[0] == "persistentState":
31 return state[cond[1]]
32 if cond[0] == "not":
33 return not all(conditionHandler(c) for c in cond[1:])
34 if cond[0] == "and":
35 return all(conditionHandler(c) for c in cond[1:])
36 if cond[0] == "eql":
37 return conditionHandler(cond[1]) == cond[2]
38 if cond[0] == "or":
39 return any(conditionHandler(c) for c in cond[1:])
40
41def groupHandler(group, segment=None):
42 out = []
43 if segment:
44 group.append(segment)
45 for item in group:
46 if type(item) is str and conditionHandler( preconditions.get(item,[]) ):
47 out.append(item)
48 if type(item) is dict:
49 if "segmentGroup" in item:
50 out += groupHandler(segmentGroups[item["segmentGroup"]])
51 if "precondition" in item:
52 if conditionHandler( preconditions.get(item["precondition"],[]) ):
53 out.append(item["segment"])
54 print("out="+repr(out),file=stderr)
55 return out
56
57
58def followTheStory(segment):
59 global state
60 global history
61 possibilities = []
62 if segment in moments:
63 m = moments[segment]
64 for moment in m:
65 if moment["type"] == "notification:playbackImpression":
66 state.update( moment.get("impressionData",{}).get("data", {}).get("persistent", {}) )
67 if moment["type"] == "scene:cs_bs":
68 for option in moment["choices"]:
69 state.update( option.get("impressionData",{}).get("data", {}).get("persistent", {}) )
70 if "segmentId" in option:
71 p = groupHandler([option["segmentId"]])
72 elif "sg" in option:
73 p = groupHandler(segmentGroups[option["sg"]])
74 elif moment["trackingInfo"]["optionType"] == "fakeOption":
75 continue
76 else:
77 raise Exception(option["id"])
78 possibilities += p
79 if moment["type"] == "notification:action":
80 possibilities.append(segment)
81 if segment in segmentGroups:
82 possibilities += groupHandler(segmentGroups[segment])
83 print("poss="+repr(possibilities),file=stderr)
84 if not possibilities:
85# raise Exception("hoi")
86 possibilities += groupHandler(segmentGroups["respawnOptions"])
87 return choice(possibilities)
88
89def bandersnatch():
90 global state
91 state = dict(initial_state)
92 current_segment = "1A"
93 while True:
94 state["length"] += smap[current_segment]["endTimeMs"] - smap[current_segment]["startTimeMs"]
95 if current_segment[:3].lower() == "0cr":
96 print("file '{}.mkv'".format(current_segment))
97 print("file 'IDNT.mkv'")
98 print("picked {} and added IDNT".format(current_segment), file=stderr)
99 state["length"] += 10
100 break
101 print("file '{}.mkv'".format(current_segment))
102 print("picked {}".format(current_segment), file=stderr)
103 current_segment = followTheStory(current_segment)
104 if current_segment is None:
105 break
106 return msToTS(state["length"])
107
108if __name__ == "__main__":
109 print(bandersnatch(),file=stderr)
110