HMS

Home Media Server for Roku Players
git clone https://www.brianlane.com/git/HMS
Log | Files | Refs | README | LICENSE

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