Beispiel #1
0
def test():
    himarket_api = HimarketApi()
    himarket_api.login()
    dummy = JsonReader.get_receipts()
    dummy_item = dummy[0]
    re = himarket_api.receipt_post(dummy_item)
    print(re.content)
Beispiel #2
0
def load_receipts():
    # 449798 receipts - First file
    receipts = JsonReader.get_receipts()
    tmp_receipts = receipts[8785:len(receipts)]
    print(len(receipts))

    for i in range(8):
        thread = ReceiptsLoader(array=tmp_receipts)
        thread.start()
Beispiel #3
0
def load_people():
    # 1168169 people
    people = JsonReader.get_consumers()
    tmp_people = people[0:len(people)]
    print(len(people))

    for i in range(4):
        thread = ConsumerLoader(array=tmp_people)
        thread.start()
Beispiel #4
0
def load_stores():
    # 543 stores
    stores = JsonReader.get_stores()
    tmp_stores = stores[0:len(stores)]
    print(len(stores))

    for i in range(4):
        thread = StoreLoader(array=tmp_stores)
        thread.start()
Beispiel #5
0
def load_products():
    products = JsonReader.get_products()

    # First file size 46411 - 2 (Two wanst loaded because of crashes)
    # So now we get the last ID and do Last_ID - 46409
    tmp_array = products[0:len(products)]
    print(len(products))

    for i in range(2):
        thread = ProductLoader(array=tmp_array)
        thread.start()
Beispiel #6
0
    def __init__(
            self,  # NOQA
            json_file: str,
            tmp_folder=TMP_FOLDER,
            no_clean=False,
            readable=False,
            no_info=False,
            silent_log=False,
            detect_framerate=True,
            verbose=False,
            quiet=False,
            vcodec=VCODEC,
            markers=False,
            use_gpu=USE_GPU,
            mac=False,
            compat_tool_sub=COMPAT_TOOL_SUBSET,
            thread_alloc=0,
            ffmpeg_path=FFMPEG_PATH,
            ffprobe_path=FFPROBE_PATH):
        """Create a Render class with all the inputs.
        To run call:
            socketThread.start()
            render()
            keep_socket_open = False
            socketThread.join()

        :param json_file: the path to the file containing all the edits
        :param tmp_folder: the location to store temp files
        :param no_clean: set True if you want to leave temp files after completioin
        :param readable: set true to output readable ffmpeg logs
        :param no_info: a dev param to skip info level logs in the stdout
        :param silent_log: output nothing to stdout nor stderr
        :param detect_framerate: let ffprobe detect the framerate
        :param verbose: pipe ffmpeg stdout to the stdout
        :param quiet: set the ffmpeg log level to error
        :param vcodec: the video codec to output
        :param markers: export to markers rather than cuts
        :param use_gpu: force the nvenc codec onto the vcodec
        :paran mac: force the videotoolbox codec onto the vcodec
        :param compat_tool_sub: a compatability measure to overcome the max shell length while running ffmpeg
        :param thread_alloc: force ffmpeg to use only n threads where n=0 is all
        :param ffmpeg_path: the path to ffmpeg
        :param ffprobe_path: the path to ffprobe
        """
        self.json_file = json_file
        self.tmp_folder = tmp_folder
        if not os.path.isdir(self.tmp_folder):
            os.mkdir(self.tmp_folder)
        self.clean_tmp = not no_clean
        pid = os.getpid()
        self.keep_socket_open = True

        def process():
            while self.socket is None:
                pass
            with self.socket:
                while self.keep_socket_open:
                    try:
                        data = self.socket.recv(1024)
                        if not data:
                            break
                        if data.decode("utf-8") == 'exit':
                            break
                    except OSError:
                        pass
                return os.kill(pid, signal.SIGKILL)

        self.socketThread = threading.Thread(target=process)
        self.socketThread.daemon = True

        def socket_callback(socket):
            self.socket = socket

        self.logger = create_logger("render", tmp_folder, no_info, silent_log,
                                    socket_callback)
        self.verbosity = ('info', 'quiet')[quiet]
        self.detect_framerate = detect_framerate
        self.pipe_ffmpeg = verbose
        self.readable = readable
        self.vcodec = vcodec
        self.markers = markers
        self.max_tool_time = compat_tool_sub
        self.ffmpeg_path = ffmpeg_path
        self.ffprobe_path = ffprobe_path
        self.thread_alloc = thread_alloc
        self.jsonReader = JsonReader(self.logger)
        if use_gpu:
            self.vcodec = self.vcodec + '_nvenc'
        if mac:
            self.vcodec = self.vcodec + '_videotoolbox'

        # TODO: ideally this will have all of the information about the stream
        self.streamInfo = StreamInfo(self.logger, self.detect_framerate,
                                     self.ffprobe_path, self.vcodec,
                                     self.resolution, self.frame_rate)
Beispiel #7
0
class Render:
    frame_rate = 29.97
    """if not detect_framerate
    the set frame rate
    else the rate from the last ffprobe action"""
    detect_framerate = False
    """force operations to use a preset rate or detect with ffprobe"""
    avg_frame_rate: float = None
    """the average rate detected by the last ffprobe action"""
    nb_frames: int = None
    """the total number of frames detected by the last ffprobe action"""
    current_codec = None
    """the chosen codec"""
    total_time = 0
    """the leng detected by the last ffprobe action"""
    bit_rate: int = None
    """the bit rate detected by the last ffprobe action"""
    longest_time = 0
    """longest length of a video (necessary for layering)"""
    total_layers = 0
    """the count of video layers"""
    tmp_files = []
    """a running list of files to be cleaned after each operation"""
    sounded_speed = 1.3  # TODO: make this inifinate capable
    """the speed of souned sections"""
    silent_speed = 0.6  # TODO: make this inifinate capable
    """the speed of silent sections"""
    markers = False
    """output to markers rather than cuts"""
    resolution = None
    """the resolution detected by the last ffprobe action"""
    socket = None
    """the current logging socket"""
    thread_alloc = 0
    """the number of treads to allocate where 0=all"""
    def __init__(
            self,  # NOQA
            json_file: str,
            tmp_folder=TMP_FOLDER,
            no_clean=False,
            readable=False,
            no_info=False,
            silent_log=False,
            detect_framerate=True,
            verbose=False,
            quiet=False,
            vcodec=VCODEC,
            markers=False,
            use_gpu=USE_GPU,
            mac=False,
            compat_tool_sub=COMPAT_TOOL_SUBSET,
            thread_alloc=0,
            ffmpeg_path=FFMPEG_PATH,
            ffprobe_path=FFPROBE_PATH):
        """Create a Render class with all the inputs.
        To run call:
            socketThread.start()
            render()
            keep_socket_open = False
            socketThread.join()

        :param json_file: the path to the file containing all the edits
        :param tmp_folder: the location to store temp files
        :param no_clean: set True if you want to leave temp files after completioin
        :param readable: set true to output readable ffmpeg logs
        :param no_info: a dev param to skip info level logs in the stdout
        :param silent_log: output nothing to stdout nor stderr
        :param detect_framerate: let ffprobe detect the framerate
        :param verbose: pipe ffmpeg stdout to the stdout
        :param quiet: set the ffmpeg log level to error
        :param vcodec: the video codec to output
        :param markers: export to markers rather than cuts
        :param use_gpu: force the nvenc codec onto the vcodec
        :paran mac: force the videotoolbox codec onto the vcodec
        :param compat_tool_sub: a compatability measure to overcome the max shell length while running ffmpeg
        :param thread_alloc: force ffmpeg to use only n threads where n=0 is all
        :param ffmpeg_path: the path to ffmpeg
        :param ffprobe_path: the path to ffprobe
        """
        self.json_file = json_file
        self.tmp_folder = tmp_folder
        if not os.path.isdir(self.tmp_folder):
            os.mkdir(self.tmp_folder)
        self.clean_tmp = not no_clean
        pid = os.getpid()
        self.keep_socket_open = True

        def process():
            while self.socket is None:
                pass
            with self.socket:
                while self.keep_socket_open:
                    try:
                        data = self.socket.recv(1024)
                        if not data:
                            break
                        if data.decode("utf-8") == 'exit':
                            break
                    except OSError:
                        pass
                return os.kill(pid, signal.SIGKILL)

        self.socketThread = threading.Thread(target=process)
        self.socketThread.daemon = True

        def socket_callback(socket):
            self.socket = socket

        self.logger = create_logger("render", tmp_folder, no_info, silent_log,
                                    socket_callback)
        self.verbosity = ('info', 'quiet')[quiet]
        self.detect_framerate = detect_framerate
        self.pipe_ffmpeg = verbose
        self.readable = readable
        self.vcodec = vcodec
        self.markers = markers
        self.max_tool_time = compat_tool_sub
        self.ffmpeg_path = ffmpeg_path
        self.ffprobe_path = ffprobe_path
        self.thread_alloc = thread_alloc
        self.jsonReader = JsonReader(self.logger)
        if use_gpu:
            self.vcodec = self.vcodec + '_nvenc'
        if mac:
            self.vcodec = self.vcodec + '_videotoolbox'

        # TODO: ideally this will have all of the information about the stream
        self.streamInfo = StreamInfo(self.logger, self.detect_framerate,
                                     self.ffprobe_path, self.vcodec,
                                     self.resolution, self.frame_rate)

    def clean_tmp_files(self, tmp_files=None):
        """clear the tmp files created during this run"""
        if tmp_files is None:
            tmp_files = self.tmp_files
        self.logger.debug("clean_tmp_files -\n{}".format(tmp_files))
        for tmp_file in tmp_files:
            if os.path.exists(tmp_file):
                os.remove(tmp_file)

    def ffmpeg_out(self,
                   current_layer: int,
                   *args,
                   override_nb_frames=False,
                   **kwargs):  # NOQA
        """output with ffmpeg
        This takes in all normal ffmpeg output args.

        This is complex because we have to deal with default args and multiple output styles

        :param current_layer: the current index of the operation
        :param total_layers: the number of total layers to offset progress
        """
        total_layers = kwargs.pop('total_layers', self.total_layers)
        self.logger.debug("ffmpeg_out - {}/{}".format(current_layer,
                                                      total_layers))
        if self.ffmpeg_path != 'ffmpeg':
            self.logger.debug("direct ffmpeg")
            assert (os.path.isfile(self.ffmpeg_path)), 'invalid ffmpeg path'

        kwargs['vcodec'] = kwargs.get('vcodec', self.vcodec)
        kwargs['threads'] = kwargs.get('threads', self.thread_alloc)
        if self.current_codec != kwargs['vcodec']:
            self.logger.debug("vcodec")
            if self.bit_rate is not None:
                kwargs['b'] = self.bit_rate
                kwargs['maxrate'] = self.bit_rate
                kwargs['bufsize'] = 2 * self.bit_rate
            kwargs['profile'] = 'high'
            kwargs['preset'] = 'slow'

        self.logger.debug("ffmpeg args - \n" + json.dumps(kwargs, indent=2))

        ffout = ffmpeg.output(*args, **kwargs).global_args(
            '-loglevel', self.verbosity, '-hide_banner').overwrite_output()
        if self.pipe_ffmpeg:
            ffmpeg.run(ffout, cmd=self.ffmpeg_path, overwrite_output=True)
        else:
            try:
                args = ffmpeg.compile(ffout,
                                      cmd=self.ffmpeg_path,
                                      overwrite_output=True)
                process = subprocess.Popen(args,
                                           stdout=subprocess.PIPE,
                                           stderr=subprocess.STDOUT,
                                           universal_newlines=True)

                for line in process.stdout:
                    if 'frame' in line and 'speed' in line:
                        groups = re.search(
                            r'frame=\s*([0-9]*).*speed=\s*([0-9.]*)',
                            line)  # noqa W605
                        frame_percent = int(groups.group(1)) / (
                            self.total_time * self.frame_rate)
                        if not override_nb_frames and self.nb_frames is not None and self.nb_frames > 0:
                            frame_percent = int(
                                groups.group(1)) / self.nb_frames
                        float_speed = 0.0
                        try:
                            float_speed = float(groups.group(2))
                        except ValueError:
                            pass

                        real_percent = frame_percent / total_layers + current_layer / total_layers
                        if self.readable:
                            sys.stdout.write("\033[K")  # clear previous line
                            self.logger.info(
                                '\r({}/{}) action progress is {:.3%} at a speed of {:}x'
                                .format(current_layer, total_layers,
                                        frame_percent, float_speed))
                        else:
                            self.logger.info('{:.3%}|{:.3f}x'.format(
                                real_percent, float_speed))
                        self.logger.handlers[1].flush()
            except Exception as e:
                self.logger.exception(e)
                raise

    def cut_clips(self, edits, video=None, audio=None):  # NOQA
        self.logger.debug(self.total_time)
        edits.append({"start": edits[-1]["end"], "end": self.total_time})
        self.total_time = 0
        video_count = 0
        audio_count = 0
        video_split = None
        audio_split = None
        self.logger.debug(
            "cut_clips -\n\tcut video - {}\n\tcut audio -{}".format(
                video, audio))
        if video is not None:
            video_split = video.filter_multi_output('split')
            video_count += 1
        if audio is not None:
            audio_split = audio.filter_multi_output('asplit')
            audio_count += 1
        try:
            cuts = []
            edit_sounded = self.sounded_speed is not None
            edit_silent = self.silent_speed is not None
            for i, edit in enumerate(edits):
                sounded_index = i
                silent_index = i
                if i >= len(edits) - 1:
                    break
                self.logger.debug("DSF: " + str(i))
                if edit_sounded and edit_silent:
                    sounded_index = i * 2
                    silent_index = sounded_index + 1

                if edit_sounded:
                    self.total_time += (edit['end'] -
                                        edit['start']) / self.sounded_speed
                    if video_split is not None:
                        video_cut = video_split[sounded_index].trim(
                            start=edit['start'],
                            end=edit['end']).setpts('PTS-STARTPTS').setpts(
                                "{}*PTS".format(1 / self.sounded_speed))
                        cuts.append(video_cut)
                    if audio_split is not None:
                        audio_cut = audio_split[sounded_index].filter(
                            'atrim', start=edit['start'],
                            end=edit['end']).filter(
                                'asetpts', expr='PTS-STARTPTS').filter(
                                    'atempo', self.sounded_speed)
                        cuts.append(audio_cut)
                if edit_silent:
                    self.total_time += (edits[i + 1]['start'] -
                                        edit['start']) / self.silent_speed
                    if video_split is not None:
                        video_cut_between = video_split[silent_index].trim(
                            start=edit['end'], end=edits[
                                i + 1]['start']).setpts('PTS-STARTPTS').setpts(
                                    "{}*PTS".format(1 / self.silent_speed))
                        cuts.append(video_cut_between)
                    if audio_split is not None:
                        audio_cut_between = audio_split[silent_index].filter(
                            'atrim',
                            start=edit['end'],
                            end=edits[i + 1]['start']).filter(
                                'asetpts', expr='PTS-STARTPTS').filter(
                                    'atempo', self.silent_speed)
                        cuts.append(audio_cut_between)
            edited = ffmpeg.concat(*cuts, v=video_count, a=audio_count)
            return edited
        except Exception as e:
            self.logger.exception(e)
            raise e

    def chunk_operation(self,
                        cut_tuple,
                        layer_id: int,
                        appx_cut_count: float,
                        cur_min: int,
                        hr_op: int,
                        adjusted_total: int,
                        list_of_cuts=[]):
        """Chunk the cut operations into smaller time-based sections
        :param cut_tuple: (per_minute_cuts, video, audio)
        :param layer_id: the current layer
        :param appx_cut_count: the guessed ammount of cuts necessary
        :param cur_min: the current minute index of the chunk
        :param hr_op: i dont remember
        :param list_of_cuts: the running total of cuts for ffmpeg to concat
        adjusted_total: the count of cuts adjusted for the number of streams
        """
        edits = [self.cut_clips(*cut_tuple)]
        adjusted_current = layer_id * appx_cut_count * 2 + cur_min - 1
        cut_name = "{}_{}_{}{}".format(hr_op[0], layer_id, cur_min, hr_op[1])
        cut_input = ffmpeg.input(cut_name)
        list_of_cuts.append(cut_input['v'])
        list_of_cuts.append(cut_input['a'])
        self.logger.debug("jumpcut - {}".format(cut_name))
        self.ffmpeg_out(adjusted_current,
                        *edits,
                        cut_name,
                        total_layers=adjusted_total)
        return adjusted_current

    def apply_operation(self,
                        layer,
                        layer_id,
                        fps,
                        resolution=None,
                        speedrun=False):  # NOQA
        """create the ffmpeg options"""
        file_to_op = '{}'.format(layer['sourceFile'])
        opType = "none"
        try:
            if 'speed' in layer:
                layer_speed = layer['speed']
                # assert('sounded' in layer_speed), "speed object must have a `souneded` parameter"
                # assert('silent' in layer_speed), "speed object must have a `silent` parameter"
                self.sounded_speed = layer_speed.get('sounded',
                                                     self.sounded_speed)
                self.silent_speed = layer_speed.get('silent',
                                                    self.silent_speed)
                if self.sounded_speed == self.silent_speed:
                    # assert(self.sounded_speed is not None), "Malformatted speed is impossible."
                    if self.sounded_speed is None:
                        self.sounded_speed = 1
                        self.logger.warning(
                            "Malformatted speed is impossible.")
                    self.logger.warning(
                        "An equal sounded and silent speed is likely a waste of resources."
                    )
            else:
                self.sounded_speed = 1
                self.silent_speed = None
            assert (os.path.exists(file_to_op)), "cannot find operation file"
            opType = layer['opType']
            self.logger.debug("apply_operation - {} - {}".format(
                opType, file_to_op))
        except Exception as e:
            self.logger.exception(e)
            raise e
        source_file = ffmpeg.input(file_to_op)
        # op_format = ('.mkv', '.ts')[speedrun]
        # mp4 mkv mov avi mp3
        # op_format = self.vcodec
        # op_format = '.mp4'
        op_format = Path(self.outFile).suffix
        output_location = self.tmp_folder + '/test_' + str(
            layer_id) + op_format
        edits = []
        audio = None
        video = None
        total_layers = self.total_layers

        try:
            if self.streamInfo.has_stream():
                resolution = resolution or self.streamInfo.resolution
                video = source_file.video
                assert (
                    'x' in resolution
                ), 'resolution should be a height and width separated by \'x\''
                dimensions = resolution.split('x')
                assert (len(dimensions) == 2
                        ), 'resolution should have 2 dimensions'
                width = dimensions[0]
                height = dimensions[1]
                video = video.filter('fps', fps=str(fps)).filter(
                    'scale',
                    width='min(' + width + ',iw)',
                    height='min(' + height + ',ih)',
                    force_original_aspect_ratio='decrease').filter(
                        'pad',
                        width=width,
                        height=height,
                        x='(ow-iw)/2',
                        y='(oh-ih)/2')
                if opType != 'jumpcutter':
                    video = video.setpts("{}*PTS".format(
                        1 / (self.sounded_speed or self.silent_speed)))
                edits.append(video)

            self.streamInfo.set_file_name(layer['sourceFile'])
            self.streamInfo.set_stream('a')
            if self.streamInfo.has_stream():
                audio = source_file.audio
                if opType != 'jumpcutter':
                    audio = audio.filter("atempo", self.sounded_speed)
                edits.append(audio)
        except Exception as e:
            self.logger.exception(e)
            raise e
        try:

            if opType == 'jumpcutter':
                try:
                    assert (video is not None and audio
                            is not None), "jumpcutter needs video and audio"
                except Exception as e:
                    self.logger.error(e)
                all_cuts = layer['cuts']
                cur_min = 1
                per_minute_cuts = []
                hr_op = output_location.split(str(layer_id))
                appx_cut_count = self.streamInfo.total_time / self.max_tool_time
                appx_cut_count = (appx_cut_count, 1)[appx_cut_count < 1]
                adjusted_total = appx_cut_count * (
                    self.total_layers
                ) * 2  # TODO: check if total_layers needs to be in streamInfo
                list_of_cuts = []
                for cut in all_cuts:
                    per_minute_cuts.append(cut)
                    if cut['start'] > self.max_tool_time * cur_min:
                        adjusted_current = self.chunk_operation(
                            (per_minute_cuts, video, audio),
                            layer_id,
                            appx_cut_count,
                            cur_min,
                            hr_op,
                            adjusted_total,
                            list_of_cuts,
                        )
                        cur_min += 1
                        per_minute_cuts = []
                # trimstart first ... last trimend
                if len(per_minute_cuts) > 0:
                    adjusted_current = self.chunk_operation(
                        (per_minute_cuts, video, audio),
                        layer_id,
                        appx_cut_count,
                        cur_min,
                        hr_op,
                        adjusted_total,
                        list_of_cuts,
                    )
                else:
                    adjusted_current = layer_id * appx_cut_count * 2 + cur_min

                edits = [ffmpeg.concat(*list_of_cuts, v=1, a=1)]

                layer_id = adjusted_current + 1
                total_layers = adjusted_total
        except Exception as e:
            self.logger.exception(e)
            raise
        try:
            opped_layer = {**layer, 'sourceFile': output_location}
            self.logger.debug("operation finilize -\n{}".format(
                json.dumps(opped_layer, indent=2)))
            self.ffmpeg_out(layer_id,
                            *edits,
                            output_location,
                            total_layers=total_layers)
            return opped_layer
        except Exception as e:
            self.logger.exception(e)
            raise

    def offset_layer(self, layer_data, i, resolution=None):
        resolution = resolution or self.streamInfo.resolution
        try:
            assert ('sourceFile'
                    in layer_data), "missing sourceFile in layer_data"
            assert ('timelineStart'
                    in layer_data), "missing timelineStart in layer_data"
        except Exception as e:
            self.logger.exception(e)
            raise
        source = layer_data['sourceFile']
        offset = layer_data['timelineStart']
        offset_seconds = float(offset) / 1000
        self.logger.debug("offset_layer - {}".format(offset_seconds))
        layer = ffmpeg.input(source)
        box = ffmpeg.input(
            '[email protected]:size={},format=rgba'.format(resolution),
            f='lavfi',
            t=str(offset_seconds + 2))
        splits = {'video': None, 'audio': None}
        self.streamInfo.set_file_name(source)
        if self.streamInfo.has_stream():
            video = layer.video.setpts(
                'PTS-STARTPTS+{}/TB'.format(offset_seconds))
            transparent_offset = ffmpeg.overlay(box,
                                                video,
                                                eof_action='repeat')
            video_split = transparent_offset.filter_multi_output('split')
            splits['video'] = [video_split[0], video_split[1]]

        self.streamInfo.set_file_name(source)
        self.streamInfo.set_stream('a')
        if self.streamInfo.has_stream():
            audio = layer.audio.filter('adelay',
                                       delays='{}:all=1'.format(offset))
            audio_split = audio.filter_multi_output('asplit')[0]
            splits['audio'] = audio_split
        self.total_time += offset / 1000
        return splits

    def overlay_layers(self, parent_split, overlay_split):
        output_splits = {'video': None, 'audio': None}
        self.logger.debug("overlay_layers")

        video_count = 0
        if parent_split['video'] is not None:
            video_count += 1
            output_splits['video'] = [
                parent_split['video'][0], parent_split['video'][1]
            ]
        if overlay_split['video'] is not None:
            video_count += 1
            output_splits['video'] = [
                overlay_split['video'][0], overlay_split['video'][1]
            ]
        if video_count > 1:
            try:
                self.logger.debug("overlay video")
                top_overlay_0 = ffmpeg.overlay(parent_split['video'][0],
                                               overlay_split['video'][0],
                                               eof_action='pass')
                bottom_overlay_0 = ffmpeg.overlay(parent_split['video'][1],
                                                  overlay_split['video'][1],
                                                  eof_action='repeat')
                output_splits['video'] = [top_overlay_0, bottom_overlay_0]
            except Exception as e:
                self.logger.exception(e)
                raise

        audio_count = 0
        if parent_split['audio'] is not None:
            audio_count += 1
            output_splits['audio'] = parent_split['audio']
        if overlay_split['audio'] is not None:
            audio_count += 1
            output_splits['audio'] = overlay_split['audio']
        if audio_count > 1:
            try:
                self.logger.debug("overlay audio")
                combined_audio = ffmpeg.filter(
                    [parent_split['audio'], overlay_split['audio']],
                    'amix',
                    inputs=2,
                    duration='longest')
                output_splits['audio'] = combined_audio
            except Exception as e:
                self.logger.exception(e)
                raise
        return output_splits

    def render(self, speedrun=False):  # NOQA
        """This is how we generate a file directly:
            Before the for loop we setup and read inputps
            The first for loop generates the ffmpeg options to
            The second for loop layers the videos
            Where we run ffmpeg_out, we start the real rendering
        """
        # FIXME: this could be done in the constructor
        _, outFile, self.frame_rate, layers, resolution = self.jsonReader.get_data(
            self.json_file)

        if resolution:
            self.streamInfo.resolution = resolution
        frame_rate = str(self.streamInfo.frame_rate)
        # FIXME: classwide outfile does not exist in export
        self.outFile = outFile

        flat_layers = [item for sublist in layers for item in sublist]
        operation_data = []
        self.total_layers = len(flat_layers) + 1

        try:
            assert (self.total_layers >
                    1), 'cannot run operation with 0 layers'
        except Exception as e:
            self.logger.exception(e)
            raise

        for i, layer in enumerate(flat_layers):
            self.sounded_speed = 1
            self.silent_speed = None
            opped_layer = self.apply_operation(layer, i, frame_rate,
                                               resolution, speedrun)
            self.tmp_files.append(opped_layer['sourceFile'])
            operation_data.append(opped_layer)

        output = []
        parent = self.offset_layer(operation_data[-1], i, resolution)
        reversed_operations = reversed(operation_data)
        if len(operation_data) > 1:
            for i, op in enumerate(reversed_operations):
                if i > 0:
                    overlay = self.offset_layer(op, i, resolution)
                    overlayed = self.overlay_layers(parent, overlay)
                    parent = overlayed
                if self.streamInfo.total_time > self.longest_time:
                    self.longest_time = self.streamInfo.total_time
            try:
                self.logger.debug("final overlay -\n{}\n{}".format(
                    parent['video'][1], parent['video'][0]))
                parent['video'][0] = ffmpeg.overlay(parent['video'][1],
                                                    parent['video'][0],
                                                    eof_action='pass')
            except Exception as e:
                self.logger.exception(e)
                raise
            self.streamInfo.total_time = self.longest_time

        try:
            self.logger.debug("creating final output")
            if parent['video'] and len(parent['video']) > 0:
                output_video = parent['video'][0].filter('fps',
                                                         fps=str(frame_rate))
                output.append(output_video)
            if parent['audio'] is not None:
                output.append(parent['audio'])

            self.ffmpeg_out(self.total_layers - 1,
                            *output,
                            outFile,
                            override_nb_frames=True)

        except Exception as e:
            self.logger.exception(e)
            raise

        if self.clean_tmp:
            try:
                self.logger.debug("cleaning -\n{}".format(
                    json.dumps(self.tmp_files)))
                self.clean_tmp_files(self.tmp_files)
            except Exception as e:
                self.logger.exception(e)
                raise
        self.logger.debug("COMPLETE\n")
        self.logger.info("")

    # export is the last function to ever call has_stream.
    def export(self, name: str = None):  # NOQA
        """This is a nightmare so here's what's happening:
            Before the for loop we're doing setup
            There are 2 for loops to deal with layering
            The inner for loop sets up each layer then at its last try, it creates a Track
            The outer for loops gathers the Tracks and creates a Timeline
            The timeline can be exported to files
        Good luck bruddah.
        """

        exporter = export_xml.XmlExporter(self.logger, self.jsonReader,
                                          self.streamInfo, self.silent_speed,
                                          self.sounded_speed, self.markers)

        exporter.export(self.json_file)