makebif.py (5733B)
1 #!/usr/local/bin/python3.12 2 """ 3 Create .bif files for Roku video streaming 4 Copyright 2009-2013 by Brian C. Lane <bcl@brianlane.com> 5 All Rights Reserved 6 7 8 makebif.py --help for arguments 9 10 Requires ffmpeg to be in the path 11 12 NOTE: The jpg image sizes are set to the values posted by bbefilms in the Roku 13 development forums. They may or may not be correct for your video aspect ratio. 14 They don't look right for me when I set the video height to 480 15 """ 16 import array 17 from optparse import OptionParser 18 import os 19 from pathlib import Path 20 import shutil 21 import struct 22 from subprocess import check_output 23 import tempfile 24 25 # for mode 0, 1, 2, 3 26 videoSizes = [(240,180), (320,240), (240,136), (320,180)] 27 28 # Extension to add to the file for mode 0, 1, 2, 3 29 modeExtension = ['SD', 'HD', 'SD', 'HD'] 30 31 32 33 def getMP4Info(filename): 34 """ 35 Get mp4 info about the video 36 37 mp4info is in the mp4v2 package 38 """ 39 details = { 'type':"", 'length':0, 'bitrate':1500, 'format':"", 'size':""} 40 output = check_output(["mp4info", filename], text=True) 41 # Parse the results 42 for line in output.split('\n'): 43 fields = line.split(None, 2) 44 try: 45 if fields[1] == 'video': 46 # parse the video info 47 # MPEG-4 Simple @ L3, 5706.117 secs, 897 kbps, 712x480 @ 23.9760 24 fps 48 videoFields = fields[2].split(',') 49 details['type'] = videoFields[0] 50 details['length'] = float(videoFields[1].split()[0]) 51 details['bitrate'] = float(videoFields[2].split()[0]) 52 details['format'] = videoFields[3] 53 details['size'] = videoFields[3].split('@')[0].strip() 54 except: 55 pass 56 57 return details 58 59 60 def extractImages( videoFile, directory, interval, mode=0, offset=0 ): 61 """ 62 Extract images from the video at 'interval' seconds 63 64 @param mode 0=SD 4:3 1=HD 4:3 2=SD 16:9 3=HD 16:9 65 @param directory Directory to write images into 66 @param interval interval to extract images at, in seconds 67 @param offset offset to first image, in seconds 68 """ 69 size = "x".join([str(i) for i in videoSizes[mode]]) 70 cmd = ["ffmpeg", "-i", videoFile, "-ss", "%d" % offset, 71 "-r", "%0.2f" % (1.00/interval), "-s", size, "%s/%%08d.jpg" % directory] 72 print(cmd) 73 output = check_output(cmd, text=True) 74 print(output) 75 76 77 def makeBIF( filename, directory, interval ): 78 """ 79 Build a .bif file for the Roku Player Tricks Mode 80 81 @param filename name of .bif file to create 82 @param directory Directory of image files 00000001.jpg 83 @param interval Time, in seconds, between the images 84 """ 85 magic = [0x89,0x42,0x49,0x46,0x0d,0x0a,0x1a,0x0a] 86 version = 0 87 88 files = os.listdir("%s" % (directory)) 89 images = [] 90 for image in files: 91 if image[-4:] == '.jpg': 92 images.append(image) 93 images.sort() 94 images = images[1:] 95 96 with open(filename, "wb") as f: 97 array.array('B', magic).tofile(f) 98 f.write(struct.pack("<I", version)) 99 f.write(struct.pack("<I", len(images))) 100 f.write(struct.pack("<I", 1000 * interval)) 101 array.array('B', [0x00 for x in range(20,64)]).tofile(f) 102 103 bifTableSize = 8 + (8 * len(images)) 104 imageIndex = 64 + bifTableSize 105 timestamp = 0 106 107 # Get the length of each image 108 for image in images: 109 statinfo = os.stat("%s/%s" % (directory, image)) 110 f.write(struct.pack("<I", timestamp)) 111 f.write(struct.pack("<I", imageIndex)) 112 113 timestamp += 1 114 imageIndex += statinfo.st_size 115 116 f.write(struct.pack("<I", 0xffffffff)) 117 f.write(struct.pack("<I", imageIndex)) 118 119 # Now copy the images 120 for image in images: 121 data = open("%s/%s" % (directory, image), "rb").read() 122 f.write(data) 123 124 def main(): 125 """ 126 Extract jpg images from the video and create a .bif file 127 """ 128 parser = OptionParser() 129 parser.add_option( "-m", "--mode", dest="mode", type='int', default=0, 130 help="(0=SD) 4:3 1=HD 4:3 2=SD 16:9 3=HD 16:9") 131 parser.add_option( "-i", "--interval", dest="interval", type='int', default=10, 132 help="Interval between images in seconds (default is 10)") 133 parser.add_option( "-o", "--offset", dest="offset", type='int', default=0, 134 help="Offset to first image in seconds (default is 7)") 135 136 (options, args) = parser.parse_args() 137 138 # Get the video file to operate on 139 videoFile = args[0] 140 print("Creating .BIF file for %s" % (videoFile)) 141 142 # This may be useful for determining the video format 143 # Get info about the video file 144 videoInfo = getMP4Info(videoFile) 145 if videoInfo["size"]: 146 size = videoInfo["size"].split("x") 147 aspectRatio = float(size[0]) / float(size[1]) 148 width, height = videoSizes[options.mode] 149 height = int(width / aspectRatio) 150 videoSizes[options.mode] = (width, height) 151 152 tmpDirectory = tempfile.mkdtemp() 153 154 # Extract jpg images from the video file 155 extractImages( videoFile, tmpDirectory, options.interval, options.mode, options.offset ) 156 157 bifFile = "%s-%s.bif" % (os.path.basename(videoFile).rsplit('.',1)[0], modeExtension[options.mode]) 158 159 # Create the BIF file 160 makeBIF( bifFile, tmpDirectory, options.interval ) 161 162 # Clean up the temporary directory 163 shutil.rmtree(tmpDirectory) 164 165 # Move it next to the source 166 dest = Path(os.path.dirname(videoFile)) 167 if not os.path.exists(dest / bifFile): 168 shutil.move(bifFile, dest) 169 else: 170 print(f"Not moving {bifFile}, already exists at {dest}") 171 172 173 if __name__ == '__main__': 174 main() 175