summaryrefslogtreecommitdiff
path: root/mediabrowser/ffmpeg.py
blob: 841c2032e252474d23a4f85de310182e4f5f9f0c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import json
import re
import codecs
import logging
from subprocess import Popen, PIPE, DEVNULL
import shlex

utf8reader = codecs.getreader('utf-8')


def LoggedPopen(command, *args, **kwargs):
    logging.info("Popen(command={} args={}) with kwargs={}".format(
        " ".join(map(repr, command)),
        " ".join(map(repr, args)),
        " ".join(map(repr, kwargs))))
    return Popen(command, *args, **kwargs)


def ffprobe_data(ospath):
    logging.info('ffprobe %s', ospath)
    process = LoggedPopen(['ffprobe', '-v', 'quiet', '-print_format', 'json',
                           '-show_format', '-show_streams', ospath], stdout=PIPE, stderr=DEVNULL)
    data = json.load(utf8reader(process.stdout))
    assert process.wait() == 0, "ffprobe failed"
    process.stdout.close()
    return data


def stream(ospath, ss, t):
    logging.info('start ffmpeg stream h264 480p on path=%s ss=%s t=%s', ospath, ss, t)
    t_2 = t + 2.0
    output_ts_offset = ss
    cutter = LoggedPopen(
        shlex.split("ffmpeg -ss {ss:.6f} -i ".format(**locals())) +
        [ospath] +
        shlex.split("-c:a aac -strict experimental -ac 2 -b:a 44k"
                    " -c:v libx264 -pix_fmt yuv420p -profile:v high -level 4.0 -preset ultrafast -trellis 0"
                    " -crf 31 -vf scale=w=trunc(oh*a/2)*2:h=360"
                    " -f mpegts"
                    " -output_ts_offset {output_ts_offset:.6f} -t {t:.6f} pipe:%d.ts".format(**locals())),
        stdout=PIPE, stderr=DEVNULL)
    return cutter


def find_next_keyframe(ospath, start, max_offset):
    """
    :param: start: start search pts as float '123.123'
    :param: max_offset: max offset after start as float
    :return: (prev_duration, pts):
                    prev_duration: duration of the frame previous to the found key-frame
                    pts: PTS of next iframe but not search longer than max_offset
    :raise: Exception if no keyframe found
    """
    logging.info("start ffprobe to find next i-frame from {}".format(start))
    if start == 0.0:
        logging.info("return (0.0, 0.0) for start == 0.0")
        return 0.0, 0.0
    process = LoggedPopen(
        shlex.split("ffprobe -read_intervals {start:.6f}%+{max_offset:.6f} -show_frames "
                    "-select_streams v -print_format flat".format(**locals())) + [ospath],
        stdout=PIPE, stderr=DEVNULL)
    data = {'frame': None}
    prev_duration = None
    try:
        line = process.stdout.readline()
        while line:
            frame, name, value = re.match('frames\\.frame\\.(\d+)\\.([^=]*)=(.*)', line.decode('ascii')).groups()
            if data['frame'] != frame:
                data.clear()
                prev_duration = None
                data['frame'] = frame

            data[name] = value

            if 'pkt_duration_time' in data:
                prev_duration = float(data['pkt_duration_time'][1:-1])

            if 'key_frame' in data and data['key_frame'] == '1' and prev_duration is not None:
                if 'pkt_pts_time' in data and data['pkt_pts_time'][1:-1] != 'N/A' and float(
                        data['pkt_pts_time'][1:-1]) > start:
                    logging.info("Found pkt_pts_time={} prev__duration={}".format(data['pkt_pts_time'], prev_duration))
                    return prev_duration, float(data['pkt_pts_time'][1:-1])
                elif 'pkt_dts_time' in data and data['pkt_dts_time'][1:-1] != 'N/A' and float(
                        data['pkt_dts_time'][1:-1]) > start:
                    logging.info("Found pkt_dts_time={} prev_duration={}".format(data['pkt_dts_time'], prev_duration))
                    return prev_duration, float(data['pkt_dts_time'][1:-1])

            line = process.stdout.readline()
        raise Exception("Failed to find next i-frame in {} .. {} of {}".format(start, start + max_offset, ospath))
    finally:
        process.stdout.close()
        process.terminate()


def calculate_splittimes(ospath, chunk_duration):
    """
    :param ospath: path to media file
    :return: list of PTS times to split the media, in the form of
                    ((start1, duration1),  (start2, duration2), ...)
                    (('24.949000', '19.500000'),  ('44.449000', ...), ...)
            Note: - start2 is equal to start1 + duration1
                  - sum(durationX) is equal to media duration
    """

    def calculate_points(media_duration):
        pos = 10
        while pos < media_duration:
            yield pos
            pos += chunk_duration

    duration = float(ffprobe_data(ospath)['format']['duration'])
    points = list(calculate_points(duration))
    adj_points = points
    # for point in points:
    #     adj_points += [find_next_iframe(ospath, point, chunk_duration / 2.0)]
    #
    for (point, nextPoint) in zip([0.0] + adj_points, adj_points + [duration]):
        yield ("{:0.6f}".format(point), "{:0.6f}".format(nextPoint - point))


def thumbnail_png(path, size):
    logging.debug("thumbnail %s", path)
    process = LoggedPopen(['ffmpegthumbnailer', '-i', path,
                           '-o', '-', '-s', str(size), '-c', 'png'],
                          stdout=PIPE, stderr=DEVNULL)
    return process