Last active 1629393043

Code to make your own Black Mirror: Bandersnatch

Revision dcc73790ee78d6bec782b548a26a70ef68f5efb8

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
1from random import choice
2import json
3from sys import stderr
4
5with open("bandersnatch.json") as f:
6 bandersnatch = json.load(f)
7
8with open("/srv/http/bi/assets/segmentmap.json") as f:
9 smap = json.load(f)
10
11initial_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 }
12state = dict(initial_state)
13
14moments = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["momentsBySegment"]
15preconditions = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["preconditions"]
16segmentGroups = bandersnatch["videos"]["80988062"]["interactiveVideoMoments"]["value"]["segmentGroups"]
17
18def msToTS(ms):
19 s,ms = divmod(ms,1000)
20 m,s = divmod(s,60)
21 h,m = divmod(m,60)
22 return "{:02d}:{:02d}:{:02d}.{:03d}".format(h,m,s,ms)
23
24def conditionHandler(cond):
25 global state
26 if not cond:
27 return True
28 if cond[0] == "persistentState":
29 return state[cond[1]]
30 if cond[0] == "not":
31 return not all(conditionHandler(c) for c in cond[1:])
32 if cond[0] == "and":
33 return all(conditionHandler(c) for c in cond[1:])
34 if cond[0] == "eql":
35 return conditionHandler(cond[1]) == cond[2]
36 if cond[0] == "or":
37 return any(conditionHandler(c) for c in cond[1:])
38
39def groupHandler(group, segment=None):
40 out = []
41 if segment:
42 group.append(segment)
43 for item in group:
44 if type(item) is str and conditionHandler( preconditions.get(item,[]) ):
45 out.append(item)
46 if type(item) is dict:
47 if "segmentGroup" in item:
48 out += groupHandler(segmentGroups[item["segmentGroup"]])
49 if "precondition" in item:
50 if conditionHandler( preconditions.get(item["precondition"],[]) ):
51 out.append(item["segment"])
52 print("out="+repr(out),file=stderr)
53 return out
54
55
56def followTheStory(segment):
57 global state
58 global history
59 possibilities = []
60 if segment in moments:
61 m = moments[segment]
62 for moment in m:
63 if moment["type"] == "notification:playbackImpression":
64 state.update( moment.get("impressionData",{}).get("data", {}).get("persistent", {}) )
65 if moment["type"] == "scene:cs_bs":
66 for option in moment["choices"]:
67 state.update( option.get("impressionData",{}).get("data", {}).get("persistent", {}) )
68 if "segmentId" in option:
69 p = groupHandler([option["segmentId"]])
70 elif "sg" in option:
71 p = groupHandler(segmentGroups[option["sg"]])
72 elif moment["trackingInfo"]["optionType"] == "fakeOption":
73 continue
74 else:
75 raise Exception(option["id"])
76 possibilities += p
77 if moment["type"] == "notification:action":
78 possibilities.append(segment)
79 if segment in segmentGroups:
80 possibilities += groupHandler(segmentGroups[segment])
81 print("poss="+repr(possibilities),file=stderr)
82 if not possibilities:
83# raise Exception("hoi")
84 possibilities += groupHandler(segmentGroups["respawnOptions"])
85 return choice(possibilities)
86
87def bandersnatch():
88 global state
89 state = dict(initial_state)
90 current_segment = "1A"
91 while True:
92 state["length"] += smap[current_segment]["endTimeMs"] - smap[current_segment]["startTimeMs"]
93 if current_segment[:3].lower() == "0cr":
94 print("file '{}.mkv'".format(current_segment))
95 print("file 'IDNT.mkv'")
96 print("picked {} and added IDNT".format(current_segment), file=stderr)
97 state["length"] += 10
98 break
99 print("file '{}.mkv'".format(current_segment))
100 print("picked {}".format(current_segment), file=stderr)
101 current_segment = followTheStory(current_segment)
102 if current_segment is None:
103 break
104 return msToTS(state["length"])
105
106if __name__ == "__main__":
107 print(bandersnatch(),file=stderr)
108