Welcome to mirror list, hosted at ThFree Co, Russian Federation.

calls.py « bpsrender « BPSRender « scripts « power_sequencer - git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 5a223dd6769a770191a4484a438665c513b400df (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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
#
# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
#
# This file is part of Power Sequencer.
#
# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with Power Sequencer. If
# not, see <https://www.gnu.org/licenses/>.
#
# IMPURE
import multiprocessing as mp
import os
import signal as sig
import subprocess as sp
from functools import partial, reduce
from itertools import chain, islice, starmap, tee
from multiprocessing import Queue

from tqdm import tqdm

from .config import LOGGER
from .helpers import BSError, checkblender, kickstart, printd, prints, printw


def chunk_frames(cfg, clargs, cmds, **kwargs):
    """
    Recover the chunk start/end frames from the constructed commands for the
    video step. This is necessary to preserve purity until later steps.

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`.
    kwargs: dict
    Dictionary with additional information from the setup step.

    Returns
    -------
    out: iter(tuple)
    Start/end pairs of frames corresponding to the chunk commands created at
    the video step.
    """
    out = map(lambda x: (x, islice(x, 1, None)), cmds)
    out = map(lambda x: zip(*x), out)
    out = map(lambda x: filter(lambda y: y[0] in ("-s", "-e"), x), out)
    out = map(lambda x: map(lambda y: int(y[1]), x), out)
    out = map(lambda x: reduce(lambda acc, y: acc + (y,), x, ()), out)
    return out


def append_chunks_file(cfg, clargs, cmds, **kwargs):
    """
    IMPURE
    Helper function for creating the chunks file that will be used by `ffmpeg`
    to concatenate the chunks into one video file.

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`.
    kwargs: dict
    MANDATORY w_frame_start, w_frame_end, ext
    Dictionary with additional information from the setup step.
    """
    with open(kwargs["chunks_file_path"], "a") as f:
        for fs, fe in chunk_frames(cfg, clargs, cmds, **kwargs):
            f.write(
                "file '{rcp}{fs}-{fe}{ext}'\n".format(
                    rcp=kwargs["render_chunk_path"].rstrip("#"),
                    fs="{fs:0{frame_pad}d}".format(fs=fs, **cfg),
                    fe="{fe:0{frame_pad}d}".format(fe=fe, **cfg),
                    **kwargs
                )
            )


def call_probe(cfg, clargs, cmds, **kwargs):
    """
    IMPURE
    Probe `clargs.blendfile` for frame start, frame end and extension (for
    video only).

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`.
    kwargs: dict
    Dictionary with additional information from the setup step.

    Returns
    -------
    out: dict
    Dictionary with info extracted from `clargs.blendfile`, namely: start
    frame, end frame and extension (only useful for video step).
    """
    kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}

    printw(cfg, "Probing")
    printw(cfg, "Input(blend) @ {}".format(clargs.blendfile), s="")
    frame_start, frame_end, ext = (0, 0, "")
    if not clargs.dry_run:
        with sp.Popen(next(cmds), **kwargs_p) as cp:
            try:
                tmp = map(partial(checkblender, "PROBE", [cfg["probe_py"]], cp), cp.stdout)
                tmp = filter(lambda x: x.startswith("BPS"), tmp)
                tmp = map(lambda x: x[4:].strip().split(), tmp)
                frame_start, frame_end, ext = chain(*tmp)
            except BSError as e:
                LOGGER.error(e)
            except KeyboardInterrupt:
                raise
            finally:
                cp.terminate()
        returncode = cp.poll()
        if returncode != 0:
            raise sp.CalledProcessError(returncode, cp.args)
    frame_start = frame_start if clargs.start is None else clargs.start
    frame_end = frame_end if clargs.end is None else clargs.end
    out = {
        "frame_start": int(frame_start),
        "frame_end": int(frame_end),
        "frames_total": int(frame_end) - int(frame_start) + 1,
        "ext": ext,
    }
    if out["ext"] == "UNDEFINED":
        raise BSError("Video extension is {ext}. Stopping!".format(ext=ext))
    printd(cfg, "Probing done")
    return out


def call_mixdown(cfg, clargs, cmds, **kwargs):
    """
    IMPURE
    Calls blender to render the audio mixdown.

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`.
    kwargs: dict
    MANDATORY render_mixdown_path
    Dictionary with additional information from the setup step.
    """
    kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}

    printw(cfg, "Rendering mixdown")
    printw(cfg, "Output @ {}".format(kwargs["render_mixdown_path"]), s="")
    if not clargs.dry_run:
        with sp.Popen(next(cmds), **kwargs_p) as cp:
            try:
                tmp = map(partial(checkblender, "MIXDOWN", [cfg["mixdown_py"]], cp), cp.stdout)
                tmp = filter(lambda x: x.startswith("BPS"), tmp)
                tmp = map(lambda x: x[4:].strip().split(), tmp)
                kickstart(tmp)
            except BSError as e:
                LOGGER.error(e)
            except KeyboardInterrupt:
                raise
            finally:
                cp.terminate()
        returncode = cp.poll()
        if returncode != 0:
            raise sp.CalledProcessError(returncode, cp.args)
    printd(cfg, "Mixdown done")


def call_chunk(cfg, clargs, queue, cmd, **kwargs):
    """
    IMPURE
    Calls blender to render one chunk (which part is determined by `cmd`).

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmd: tuple
    Tuple to be passed to `subprocess`.
    kwargs: dict
    Dictionary with additional information from the setup step.
    """
    sig.signal(sig.SIGINT, sig.SIG_IGN)
    kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}

    if not clargs.dry_run:
        # can't use nice functional syntax if we want to simplify with `with`
        with sp.Popen(cmd, **kwargs_p) as cp:
            try:
                tmp = map(
                    partial(
                        checkblender,
                        "VIDEO",
                        [cfg["video_py"], "The encoder timebase is not set"],
                        cp,
                    ),
                    cp.stdout,
                )
                tmp = filter(lambda x: x.startswith("Append frame"), tmp)
                tmp = map(lambda x: x.split()[-1], tmp)
                tmp = map(int, tmp)
                tmp = map(lambda x: True, tmp)
                kickstart(map(queue.put, tmp))
                queue.put(False)
            except BSError as e:
                LOGGER.error(e)


def call_video(cfg, clargs, cmds, **kwargs):
    """
    IMPURE
    Multi-process call to blender for rendering the (video) chunks.

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`.
    kwargs: dict
    Dictionary with additional information from the setup step.
    """
    printw(cfg, "Rendering video (w/o audio)")
    printw(cfg, "Output @ {}".format(kwargs["render_chunk_path"]), s="")
    try:
        not clargs.dry_run and os.remove(kwargs["chunks_file_path"])
        LOGGER.info("CALL-VIDEO: generating {}".format(kwargs["chunks_file_path"]))
    except OSError as e:
        LOGGER.info("CALL-VIDEO: skipping {}: {}".format(e.filename, e.strerror))

    cmds, cmds_cf = tee(cmds)
    (not clargs.dry_run and append_chunks_file(cfg, clargs, cmds_cf, **kwargs))
    # prepare queue/worker
    queues = queues_close = (Queue(),) * clargs.workers
    # prpare processes
    proc = starmap(
        lambda q, cmd: mp.Process(target=partial(call_chunk, cfg, clargs, **kwargs), args=(q, cmd)),
        zip(queues, cmds),
    )
    # split iterator in 2 for later joining the processes and sum
    # one of them
    proc, proc_close = tee(proc)
    proc = map(lambda p: p.start(), proc)
    try:
        not clargs.dry_run and kickstart(proc)

        # communicate with processes through the queues and use tqdm to show a
        # simple terminal progress bar baesd on video total frames
        queues = map(lambda q: iter(q.get, False), queues)
        queues = chain(*queues)
        queues = tqdm(queues, total=kwargs["frame_end"] - kwargs["frame_start"] + 1, unit="frames")
        not clargs.dry_run and kickstart(queues)
    except KeyboardInterrupt:
        proc_close = map(lambda x: x.terminate(), proc_close)
        not clargs.dry_run and kickstart(proc_close)
        raise
    finally:
        # close and join processes and queues
        proc_close = map(lambda x: x.join(), proc_close)
        not clargs.dry_run and kickstart(proc_close)

        queues_close = map(lambda q: (q, q.close()), queues_close)
        queues_close = starmap(lambda q, _: q.join_thread(), queues_close)
        not clargs.dry_run and kickstart(queues_close)
    printd(cfg, "Video chunks rendering done")


def call_concatenate(cfg, clargs, cmds, **kwargs):
    """
    IMPURE
    Calls ffmpeg in order to concatenate the video chunks together.

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`.
    kwargs: dict
    MANDATORY: render_video_path
    Dictionary with additional information from the setup step.

    Note
    ----
    It expects the video chunk files to already be available.
    """
    kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL}
    printw(cfg, "Concatenating (video) chunks")
    printw(cfg, "Output @ {}".format(kwargs["render_video_path"]), s="")
    if not clargs.dry_run:
        with sp.Popen(next(cmds), **kwargs_p) as cp:
            try:
                returncode = cp.wait()
                if returncode != 0:
                    raise sp.CalledProcessError(returncode, cp.args)
            except KeyboardInterrupt:
                raise
            finally:
                cp.terminate()
    printd(cfg, "Concatenating done")


def call_join(cfg, clargs, cmds, **kwargs):
    """
    IMPURE
    Calls ffmpeg for joining the audio mixdown and the video.

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`.
    kwargs: dict
    MANDATORY: render_audiovideo_path
    Dictionary with additional information from the setup step.

    Note
    ----
    It expects the audio mixdown and video files to already be available.
    """
    kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL}
    printw(cfg, "Joining audio/video")
    printw(cfg, "Output @ {}".format(kwargs["render_audiovideo_path"]), s="")
    if not clargs.dry_run:
        with sp.Popen(next(cmds), **kwargs_p) as cp:
            try:
                returncode = cp.wait()
                if returncode != 0:
                    raise sp.CalledProcessError(returncode, cp.args)
            except KeyboardInterrupt:
                raise
            finally:
                cp.terminate()
    printd(cfg, "Joining done")


def call(cfg, clargs, cmds, **kwargs):
    """
    IMPURE
    Delegates work to appropriate `call_*` functions.

    Parameters
    ----------
    cfg: dict
    Configuration dictionary.
    clargs: Namespace
    Command line arguments (normalized).
    cmds: iter(tuple)
    Iterator of commands to be passed to `subprocess`
    kwargs: dict
    MANDATORY: render_audiovideo_path
    Dictionary with additional information from the setup step.

    Returns
    -------
    out: dict or None
    It passes on the output from the `call_*` functions. See `call_*` for
    specific details.

    Note
    ----
    It tries to be smart and skip steps if child subprocesses give errors.
    Example if `--join-only` is passed, but the audio mixdown or video file
    aren't available on hard drive.
    """
    calls = {
        "probe": call_probe,
        "mixdown": call_mixdown,
        "video": call_video,
        "concatenate": call_concatenate,
        "join": call_join,
    }
    try:
        out = calls[cmds[0]](cfg, clargs, cmds[1], **kwargs)
        return out
    except sp.CalledProcessError:
        prints(
            cfg,
            ("WARNING:{}: Something went wrong when calling" " command - SKIPPING").format(cmds[0]),
        )