Skip to content

rendering

Rendering API wrapper for Blackmagic Design DaVinci Resolve.

delete_all_processed_jobs()

Delete all processed jobs

Source code in client/ayon_resolve/api/rendering.py
164
165
166
167
168
169
170
171
172
173
def delete_all_processed_jobs():
    """Delete all processed jobs"""
    bmr_project = get_current_resolve_project()
    if not _PROCESSING_JOBS:
        return

    for job_id in _PROCESSING_JOBS:
        bmr_project.DeleteRenderJob(job_id)

    _PROCESSING_JOBS.clear()

is_rendering_in_progress()

Check if rendering is in progress

Source code in client/ayon_resolve/api/rendering.py
125
126
127
128
129
130
131
def is_rendering_in_progress():
    """Check if rendering is in progress"""
    bmr_project = get_current_resolve_project()
    if not bmr_project:
        return False

    return bmr_project.IsRenderingInProgress()

modify_preset_file(xml_path, staging_dir, data)

Copy xml_path to staging_dir and apply data overrides.

Each key in data is either a bare element tag name (e.g. "NumFramesOfHandles") or a slash-separated path expression (e.g. "Parent/Child").

  • Bare name – every matching element in the document is updated.
  • Path – the leaf element at the given path is updated; if absent it is created as the first child of its parent element.

Parameters:

Name Type Description Default
xml_path Path

Source XML preset file.

required
staging_dir Path

Directory where the modified copy is written.

required
data dict

Mapping of XML tag / path → new text value.

required

Returns:

Name Type Description
Path Path

Path to the modified copy of the preset in staging_dir.

Raises:

Type Description
AttributeError

Logged as a warning when a bare tag is not found.

Source code in client/ayon_resolve/api/rendering.py
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
def modify_preset_file(
    xml_path: Path,
    staging_dir: Path,
    data: dict,
) -> Path:
    """Copy *xml_path* to *staging_dir* and apply *data* overrides.

    Each key in *data* is either a bare element tag name (e.g.
    ``"NumFramesOfHandles"``) or a slash-separated path expression (e.g.
    ``"Parent/Child"``).

    * **Bare name** – every matching element in the document is updated.
    * **Path** – the leaf element at the given path is updated; if absent
      it is created as the first child of its parent element.

    Args:
        xml_path (Path): Source XML preset file.
        staging_dir (Path): Directory where the modified copy is written.
        data (dict): Mapping of XML tag / path → new text value.

    Returns:
        Path: Path to the modified copy of the preset in *staging_dir*.

    Raises:
        AttributeError: Logged as a warning when a bare tag is not found.
    """
    temp_path = staging_dir / xml_path.name

    # Read raw text upfront so we can extract the prolog (XML declaration +
    # any prolog-level comments).  Python's ElementTree parser drops prolog
    # comments silently; we preserve them by re-attaching the prolog to the
    # written output.
    raw_text = xml_path.read_text(encoding="utf-8")
    prolog = _extract_prolog(raw_text)

    # insert_comments=True preserves comments that live *inside* elements.
    parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
    tree = ET.parse(xml_path, parser=parser)

    for key, value in data.items():
        log.debug(f"Setting {key} to {value}")
        try:
            if "/" in key:
                # Normalise to a descendant XPath expression.
                xpath = key if key.startswith("./") else f".//{key}"

                *parent_parts, leaf = xpath.split("/")
                parent_path = "/".join(parent_parts)

                parent = tree.find(parent_path)
                element = parent.find(leaf)
                if element is None:
                    _append_element(parent, leaf, value)
                else:
                    log.debug(f"Setting string 1 {key} to {value}")
                    element.text = str(value)
            else:
                elements = tree.findall(f".//{key}")
                if not elements:
                    raise AttributeError(key)
                for element in elements:
                    log.debug(f"Setting string 2 {key} to {value}")
                    element.text = str(value)
        except AttributeError:
            log.warning(f"Cannot set '{key}': tag not found. Skipping.")

    # Write the modified element tree as unicode text (no xml_declaration —
    # the original prolog, which already contains the declaration and any
    # prolog-level comments, is prepended directly).
    buf = io.StringIO()
    tree.write(buf, encoding="unicode", xml_declaration=False)
    temp_path.write_text(prolog + buf.getvalue(), encoding="utf-8")

    return temp_path

render_all_timelines(target_render_directory)

Render all of the timelines of current project.

Parameters:

Name Type Description Default
target_render_directory Path

Path to target render directory

required

Returns:

Name Type Description
bool

True if all renders are successful, False otherwise

Source code in client/ayon_resolve/api/rendering.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def render_all_timelines(target_render_directory):
    """Render all of the timelines of current project.

    Args:
        target_render_directory (Path): Path to target render directory

    Returns:
        bool: True if all renders are successful, False otherwise
    """
    bmr_project = get_current_resolve_project()
    with maintain_page_by_name("Deliver"):
        timelineCount = bmr_project.GetTimelineCount()
        all_timelines = [
            bmr_project.GetTimelineByIndex(index + 1)
            for index in range(0, int(timelineCount))
        ]
        return _render_timelines(all_timelines, target_render_directory)

render_clip_to_intermediate_file(timeline_item, target_render_directory)

Render a single TimelineItem's range on the currently active timeline.

Uses the render settings already configured on the project (format, codec, preset). Caller is responsible for setting those up before calling this function (e.g. via set_render_preset_from_file and set_format_and_codec).

Parameters:

Name Type Description Default
timeline_item TimelineItem

A Resolve TimelineItem object from the active timeline.

required
target_render_directory Path

Directory where rendered files are written.

required

Returns:

Name Type Description
Path Path | list[Path]

Single file path for container formats (QuickTime, MXF, …).

Path | list[Path]

list[Path]: Sorted list of frame paths for image sequences (EXR, DPX, …).

Raises:

Type Description
RuntimeError

If the render job cannot be created, started, or completes with a non-"Complete" status, or if no output files are found.

Source code in client/ayon_resolve/api/rendering.py
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
def render_clip_to_intermediate_file(
    timeline_item: resolve.TimelineItem,
    target_render_directory: Path
) -> Path | list[Path]:
    """Render a single TimelineItem's range on the currently active timeline.

    Uses the render settings already configured on the project (format, codec,
    preset). Caller is responsible for setting those up before calling this
    function (e.g. via ``set_render_preset_from_file`` and
    ``set_format_and_codec``).

    Args:
        timeline_item: A Resolve ``TimelineItem`` object from the active timeline.
        target_render_directory (Path): Directory where rendered files are written.

    Returns:
        Path: Single file path for container formats (QuickTime, MXF, …).
        list[Path]: Sorted list of frame paths for image sequences (EXR, DPX, …).

    Raises:
        RuntimeError: If the render job cannot be created, started, or completes
            with a non-"Complete" status, or if no output files are found.
    """
    bmr_project = get_current_resolve_project()
    media_pool_item = timeline_item.GetMediaPoolItem()

    render_settings = {
        "SelectAllFrames": False,
        "MarkIn":    timeline_item.GetStart(),
        "MarkOut":   timeline_item.GetEnd() - 1,
        "TargetDir": target_render_directory.as_posix(),
        "CustomName": timeline_item.GetName(),
        "FrameRate": float(media_pool_item.GetClipProperty("FPS")),
    }
    log.info(f"Clip render settings: {pformat(render_settings)}")

    if not bmr_project.SetRenderSettings(render_settings):
        raise RuntimeError("SetRenderSettings failed for clip render.")

    with _solo_video_track(timeline_item):
        job_id = bmr_project.AddRenderJob()
        if not job_id:
            raise RuntimeError("AddRenderJob failed for clip render.")

        log.info(f"Clip render job created: {job_id}")
        try:
            if not bmr_project.StartRendering([job_id], isInteractiveMode=False):
                raise RuntimeError(f"StartRendering failed for job '{job_id}'.")
            wait_for_rendering_completion()

            status = bmr_project.GetRenderJobStatus(job_id)
            if status.get("JobStatus") != "Complete":
                raise RuntimeError(
                    f"Clip render job '{job_id}' did not complete: {status}"
                )
        finally:
            log.info(f"Deleting clip render job: {job_id}")
            bmr_project.DeleteRenderJob(job_id)

    # Collect all rendered files, handling two layouts:
    #   1. Flat:   target_render_directory/*.<ext>
    #   2. Nested: target_render_directory/**/*.<ext>
    #              (files may live inside one or more levels of sub-folders)
    rendered = sorted(
        f for f in target_render_directory.rglob("*") if f.is_file()
    )
    if not rendered:
        msg = f"No rendered files found in '{target_render_directory}'."
        raise RuntimeError(msg)

    if rendered[0].suffix.lstrip(".").lower() in _IMAGE_SEQUENCE_EXTS:
        log.info("Clip rendered as image sequence: %d frames", len(rendered))
        return rendered  # list[Path]

    log.info("Clip rendered as single file: %s", rendered[0])
    return rendered[0]  # Path

render_single_timeline(timeline, target_render_directory)

Render single timeline

Process is taking a defined timeline and render it to temporary intermediate file which will be lately used by Extract Review plugin for conversion to review file.

Parameters:

Name Type Description Default
timeline Timeline

Timeline object

required
target_render_directory Path

Path to target render directory

required

Returns:

Name Type Description
bool

True if rendering is successful, False otherwise

Source code in client/ayon_resolve/api/rendering.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def render_single_timeline(timeline, target_render_directory):
    """Render single timeline

    Process is taking a defined timeline and render it to temporary
    intermediate file which will be lately used by Extract Review plugin
    for conversion to review file.

    Args:
        timeline (resolve.Timeline): Timeline object
        target_render_directory (Path): Path to target render directory

    Returns:
        bool: True if rendering is successful, False otherwise
    """
    return _render_timelines([timeline], target_render_directory)

render_timeline_intermediate_file(timeline, target_render_directory, preset_path, file_format, codec)

Render timeline to an intermediate file in target_render_directory.

Parameters:

Name Type Description Default
timeline Timeline

Active Resolve Timeline object.

required
target_render_directory Path

Staging directory for the output.

required
preset_path Path

Path to the render preset XML file.

required
file_format str

Resolve format name (e.g. "QuickTime").

required
codec str

Resolve codec name (e.g. "H.264").

required

Returns:

Name Type Description
Path Path | list[Path]

Path to the rendered file.

Path | list[Path]

list[Path]: List of paths to the rendered files.

Raises:

Type Description
RuntimeError

If the render preset cannot be loaded, the format and codec cannot be set, or the timeline cannot be rendered.

Source code in client/ayon_resolve/api/rendering.py
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
411
412
413
414
415
416
417
418
419
420
421
422
423
def render_timeline_intermediate_file(
    timeline: resolve.Timeline,
    target_render_directory: Path,
    preset_path: Path,
    file_format: str,
    codec: str,
) -> Path | list[Path]:
    """Render *timeline* to an intermediate file in *target_render_directory*.

    Args:
        timeline: Active Resolve Timeline object.
        target_render_directory (Path): Staging directory for the output.
        preset_path (Path): Path to the render preset XML file.
        file_format (str): Resolve format name (e.g. ``"QuickTime"``).
        codec (str): Resolve codec name (e.g. ``"H.264"``).

    Returns:
        Path: Path to the rendered file.
        list[Path]: List of paths to the rendered files.

    Raises:
        RuntimeError: If the render preset cannot be loaded, the format and codec
            cannot be set, or the timeline cannot be rendered.
    """
    log.info(f"Rendering timeline to '{target_render_directory}'")

    with maintain_page_by_name("Deliver"):
        if not set_render_preset_from_file(preset_path.as_posix()):
            raise RuntimeError("Unable to load render preset.")

        format_extension = set_format_and_codec(file_format, codec)
        if not format_extension:
            raise RuntimeError("Unable to set render format and codec.")

        if not render_single_timeline(timeline, target_render_directory):
            raise RuntimeError("Unable to render timeline.")

    # Collect all files matching the format extension, handling two layouts:
    #   1. Flat:   target_render_directory/*.{format_extension}
    #   2. Nested: target_render_directory/**/*.{format_extension}
    #              (files may live inside one or more levels of sub-folders)
    rendered_files = sorted(
        target_render_directory.rglob(f"*.{format_extension}")
    )
    if not rendered_files:
        msg = (
            f"No rendered files with extension '{format_extension}' found "
            f"in '{target_render_directory}'."
        )
        raise RuntimeError(msg)

    if len(rendered_files) > 1:
        return rendered_files

    return rendered_files[0]

wait_for_rendering_completion()

Wait for rendering completion

Source code in client/ayon_resolve/api/rendering.py
134
135
136
137
138
def wait_for_rendering_completion():
    """Wait for rendering completion"""
    while is_rendering_in_progress():
        time.sleep(_SLEEP_TIME)
    return