ВИДЕОМОНТАЖЁРDAVINCI RESOLVE · ВИДЕО · AI

RESOLVE API / QUEUE LAB / 01

Как управлять очередью рендера через Python

Одна очередь. Несколько независимых действий.

Разбираем запуск рендера по точному JobId: чтение очереди, создание, запуск, ожидание и проверка файла. Без запасного запуска всех заданий и скрытой очистки.

НА ВЫХОДЕ

Карта очереди выбранного проекта и понятный маршрут к одному запуску, а не непрозрачный скрипт «сделать всё».

PYTHON / RESOLVEУЧЕБНАЯ МОДЕЛЬ
01jobs = project.GetRenderJobList()

Сначала читаем задания. Старый таймлайн не выбирает автоматически нужную запись очереди.

ПРОВЕРКА

Точный проект и JobId.

Переключатели меняют объяснение. Ничего не подключается к Resolve и не запускается на вашем компьютере.

01 / BEFORE THE CODE

До кода — условия запуска.

01

Среда и источник

Внешний Python 3.9+ и запущенный Resolve Studio с локальным scripting. Сверьте интерфейс своей установленной версии. В этом проходе проверена опубликованная копия интерфейса Blackmagic на 6 октября 2026; живая совместимость, минимальная версия и работа в Free не заявляются.

02

Точная цель

Используйте отдельную тестовую копию и одного оператора. ID проекта и задания — точные непрозрачные строки, не номер очереди, имя таймлайна или файла. Текущий таймлайн нужен только для создания нового задания; чтению и управлению существующими заданиями он не требуется.

03

Перед изменением

Сначала прочитайте план. Применение требует точной цели, совпавшего хеша и подтверждения тестовой копии. Для add/start самостоятельно проверьте Deliver и выбранное задание. Хеш не является блокировкой, проверкой прав или полным снимком настроек.

Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]

02 / API CONTRACT

Какой объект. Какой метод. Какой результат.

ProjectGetRenderJobList()
Вход
Без аргументов.
Возвращает
Список RenderJobInfo.
Проверить
Пустой список допустим; None и неизвестная форма — не пустая очередь.
ProjectStartRendering([job_id], False)
Вход
Один точный строковый ID в списке.
Возвращает
Bool принятия запуска.
Проверить
В этом примере инвентаризации не вызывается; см. отдельный запуск.
ProjectGetRenderJobStatus(job_id)
Вход
ID существующего задания.
Возвращает
JobStatus и дополнительные поля.
Проверить
Не смешивать со статусами Quick Export.

Контракты методов Resolve описаны Blackmagic; Python и авторские helper обозначены отдельно. Дополнительные проверки — устройство наших примеров. Непустой объект не доказывает совместимость всего API.[18]

03 / UNDERSTAND

Настройки, задание и готовый файл — разные сущности

Параметры Deliver определяют конфигурацию будущего задания. AddRenderJob создаёт отдельную запись очереди и возвращает её ID. StartRendering принимает выбранные ID; принятие вызова ещё не означает завершение кодирования. Наконец, состояние Complete относится к заданию, а качество и состав файлов проверяются отдельно.

В этом материале общий пример намеренно только читает очередь. Ниже есть команды следующего шага: сначала получить план запуска уже подготовленного задания, затем применить его отдельно. Настройки вывода разобраны в предыдущей группе; скрипт не выбирает пресет вместо читателя.[18]

04 / UNDERSTAND

JobId — не порядковый номер

GetRenderJobList возвращает записи с JobId и сведениями о задании. Имена RenderJobName, TimelineName и OutputFilename помогают человеку разобраться, но не заменяют идентификатор. Две записи могут иметь похожие имена и выводить материал одного таймлайна с разными настройками.

Скрипт сохраняет ID как строку с исходным регистром. Он не берёт первый элемент, не преобразует ID в число и не угадывает jobId/JobID/Id. Если ваша установленная версия возвращает другую форму, нужен отдельный проверенный адаптер, а не случайный перебор значений.[18]

05 / UNDERSTAND

Почему убрана цепочка запасных вызовов

Раньше при неуспехе запуска по ID пример переходил к другим формам вызова и в конце вызывал StartRendering без аргументов. Так ограниченная задача могла превратиться в запуск всей очереди. Теперь выбран только документированный вариант со списком, содержащим один точный ID.

Для учебного запуска требуется локальное задание Ready и свободная очередь. Завершённое или отменённое задание не перезапускается автоматически. Нет также загрузки первого найденного пресета, создания папки и изменения параметров между просмотром очереди и запуском.[18]

06 / UNDERSTAND

Четыре уровня результата

Поле is_rendering описывает наблюдение за текущей активностью, а не результат каждого задания. Ready при свободном приложении — ожидающая запись, Failed — отдельный неуспешный исход, а потеря связи не становится «рендер закончен». Журнал нужно читать по JobId и времени наблюдения.

После Complete сопоставьте ожидаемое имя или набор файлов с фактическим выводом, откройте медиа, проверьте начало и конец, звук и свойства. Для Individual clips может быть несколько файлов. Статья не обещает, что одного существующего файла или размера больше нуля достаточно для приёмки.[18]

QUEUE / OUTCOME LAB

Один сигнал — ещё не исход.

Только учебные данные. Без подключения и команд.

ИСХОД МОДЕЛИ

continue_observation

Продолжайте ограниченное наблюдение за тем же ID. Ready при 100% и свободное приложение не доказывают завершение.

Класс заданияreadyФайл проверенНет

Поля меняют пример, не ваш проект. Автосохранения и отправки нет. Скачанный JSON — отредактированная учебная модель, не журнал Resolve, сертификат завершения или разрешение выполнить команду.

PYTHON / COMPLETE EXAMPLE

Прочитайте код. Затем запускайте осознанно.

Три Python-файла в одной папке

Положите inspect_render_queue.py, resolve_api_queue.py и resolve_api_connection.py в одну папку. Комплект содержит восемь основных скриптов, оба helper и инструкции RU/EN. Официальный SDK не включён; используется мост установленного Blackmagic.

Скачать все примеры с инструкцией (.zip) ↓
inspect_render_queue.py
Скачать .py
"""Read a bounded queue inventory. Does not create, start, stop or delete jobs."""
import argparse
from resolve_api_connection import connect
from resolve_api_queue import cli_target, inspect_queue, print_result


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    cli_target(parser)
    args = parser.parse_args()
    return print_result(lambda: inspect_queue(connect(), args.project_id, args.details))


if __name__ == "__main__":
    raise SystemExit(main())

Queue-helper — точные ID, ограниченное наблюдение и явные изменения

Авторский учебный код, не SDK. Нет запуска и удаления всей очереди. StopRendering остаётся общей командой. Выборочная очистка последовательна и может завершиться частично; неизвестные исходы не повторяются. Мягкий deadline не прерывает зависший нативный вызов.

resolve_api_queue.py
Скачать .py
"""S07 author tutorial helpers, NOT the Blackmagic SDK.

Plan-only by default. Opaque IDs; no all-queue start, preset changes, directory
creation, project save, auto retry or rollback. StopRendering is project-wide.
Selected cleanup is deliberately sequential and may partially complete.
Matching observations are not a lock or an atomic snapshot. Poll deadlines
cannot interrupt a blocked native API call. Use an idle, local test project
with one operator; remote/background workflows are outside mutation scope.
"""
from __future__ import annotations
import argparse
import copy
import hashlib
import json
import math
import time

MAX_JOBS = 1000      # Tutorial bounds, not Resolve limits.
MAX_DELETE = 25
MAX_POLLS = 10000
LOCAL_STATES = frozenset({'Ready', 'Rendering', 'Complete', 'Cancelled', 'Failed'})
CANCELLED = frozenset({'Cancelled', 'Background Render Cancelled', 'Remote Render Cancelled'})
EXTERNAL = frozenset({'Ready for background render', 'Ready to remotely render'})
VOLATILE = frozenset({'JobStatus', 'CompletionPercentage', 'TimeTakenToRenderInMs',
                     'EstimatedTimeRemainingInMs', 'Error'})


class PreflightError(RuntimeError):
    """No mutation has been sent by the failing preflight step."""


def read(obj, method, *args):
    fn = getattr(obj, method, None)
    if not callable(fn):
        raise PreflightError('Missing method: ' + method)
    try:
        return fn(*args)
    except Exception as exc:
        raise PreflightError('Read failed: ' + method + ' (' + type(exc).__name__ + ')') from exc


def text(value, name):
    if not isinstance(value, str) or not value.strip() or len(value) > 4096:
        raise PreflightError('Invalid ' + name + '; expected an exact nonempty string')
    if any(ord(c) < 32 for c in value):
        raise PreflightError('Control characters in ' + name)
    try:
        value.encode('utf-8')
    except UnicodeError as exc:
        raise PreflightError('Invalid Unicode in ' + name) from exc
    return value  # No trim, lower, int coercion or guessed aliases.


def boolean(value, name):
    if type(value) is not bool:
        raise PreflightError(name + ' must be bool; None is not False')
    return value


def json_value(value, depth=0, budget=None):
    if budget is None:
        budget = [200000]
    budget[0] -= 1
    if depth > 8 or budget[0] < 0:
        raise PreflightError('API result exceeds tutorial depth/size bounds')
    if value is None or type(value) in (bool, int):
        return value
    if isinstance(value, str) and len(value) <= 32768:
        try:
            value.encode('utf-8')
        except UnicodeError as exc:
            raise PreflightError('Invalid API Unicode') from exc
        return value
    if type(value) is float and math.isfinite(value):
        return value
    if isinstance(value, list):
        return [json_value(v, depth + 1, budget) for v in value]
    if isinstance(value, dict) and all(isinstance(k, str) for k in value):
        return {k: json_value(v, depth + 1, budget) for k, v in value.items()}
    raise PreflightError('Unsupported API JSON value; no default=str conversion')


def digest(value):
    data = json.dumps(value, ensure_ascii=False, sort_keys=True, allow_nan=False,
                      separators=(',', ':')).encode('utf-8')
    return hashlib.sha256(data).hexdigest()


def classify_status(raw):
    """Preserve unknown status. Progress/global idle alone never means success."""
    if not isinstance(raw, dict):
        raise PreflightError('GetRenderJobStatus must return a dict, not None')
    raw = json_value(raw)
    name = raw.get('JobStatus')
    progress = raw.get('CompletionPercentage')
    issues = []
    if not isinstance(name, str) or not name.strip():
        issues.append('missing_or_invalid_JobStatus')
    if progress is not None and (type(progress) not in (int, float)
                                or not math.isfinite(progress) or not 0 <= progress <= 100):
        issues.append('invalid_CompletionPercentage')
    category = {'Ready': 'ready', 'Rendering': 'rendering', 'Complete': 'complete',
                'Failed': 'failed'}.get(name, 'unknown') if isinstance(name, str) else 'unknown'
    if isinstance(name, str) and name in CANCELLED:
        category = 'cancelled'
    if isinstance(name, str) and name in EXTERNAL:
        category = 'external'
    if category == 'complete' and progress is not None and progress != 100:
        issues.append('complete_progress_conflict')
    if category == 'complete' and raw.get('Error') not in (None, ''):
        issues.append('complete_error_conflict')
    if issues:
        category = 'unknown'
    return {'category': category, 'job_status': name, 'completion_percentage': progress,
            'issues': issues, 'raw': raw, 'file_verified': False}


def target(resolve, project_id=None, timeline_id=None, need_timeline=False):
    manager = read(resolve, 'GetProjectManager')
    if not manager:
        raise PreflightError('No Project Manager')
    project = read(manager, 'GetCurrentProject')
    if not project:
        raise PreflightError('No current project')
    pid = text(read(project, 'GetUniqueId'), 'project ID')
    if project_id is not None and pid != text(project_id, 'expected project ID'):
        raise PreflightError('Active project changed or differs from --project-id')
    tid = None
    if need_timeline:
        timeline = read(project, 'GetCurrentTimeline')
        if not timeline:
            raise PreflightError('No current timeline for AddRenderJob')
        tid = text(read(timeline, 'GetUniqueId'), 'timeline ID')
        if timeline_id is not None and tid != text(timeline_id, 'expected timeline ID'):
            raise PreflightError('Active timeline differs from --timeline-id')
    return project, pid, tid


def jobs(project):
    raw = read(project, 'GetRenderJobList')
    if not isinstance(raw, list) or len(raw) > MAX_JOBS:
        raise PreflightError('Expected bounded list from GetRenderJobList; None is not []')
    result = []
    seen = set()
    for row in raw:
        if not isinstance(row, dict):
            raise PreflightError('RenderJobInfo must be a dict')
        item = json_value(row)
        jid = text(item.get('JobId'), 'JobId')
        if jid in seen:
            raise PreflightError('Duplicate JobId; cannot address a unique queue item')
        seen.add(jid)
        result.append(item)
    return result


def queue_structure(rows):
    return [{k: v for k, v in r.items() if k not in VOLATILE} for r in rows]


def snapshot(resolve, project_id=None, timeline_id=None, need_timeline=False):
    """Two queue reads bracket status reads; this is still non-atomic."""
    project, pid, tid = target(resolve, project_id, timeline_id, need_timeline)
    rows = jobs(project)
    active = boolean(read(project, 'IsRenderingInProgress'), 'IsRenderingInProgress')
    states = {r['JobId']: classify_status(read(project, 'GetRenderJobStatus', r['JobId'])) for r in rows}
    if queue_structure(jobs(project)) != queue_structure(rows):
        raise PreflightError('Queue structure changed during observation')
    if boolean(read(project, 'IsRenderingInProgress'), 'IsRenderingInProgress') != active:
        raise PreflightError('Rendering activity changed during observation')
    target(resolve, pid, tid, need_timeline)
    return project, {'project_id': pid, 'timeline_id': tid, 'is_rendering': active,
                     'jobs': rows, 'statuses': states}


def stable(snapshot_value):
    return {'project_id': snapshot_value['project_id'], 'timeline_id': snapshot_value['timeline_id'],
            'is_rendering': snapshot_value['is_rendering'],
            'jobs': queue_structure(snapshot_value['jobs']),
            'statuses': {jid: {'job_status': v['job_status'], 'category': v['category'],
                               'issues': v['issues']} for jid, v in snapshot_value['statuses'].items()}}


def public_snapshot(s, details=False):
    out = {'project_id': s['project_id'], 'timeline_id': s['timeline_id'],
           'is_rendering': s['is_rendering'], 'count': len(s['jobs']),
           'job_ids': [r['JobId'] for r in s['jobs']], 'observation_hash': digest(stable(s)),
           'statuses': {k: {a: b for a, b in v.items() if a != 'raw'} for k, v in s['statuses'].items()},
           'atomic': False, 'file_verified': False}
    if details:
        out['raw_jobs'] = copy.deepcopy(s['jobs'])
        out['raw_statuses'] = {k: copy.deepcopy(v['raw']) for k, v in s['statuses'].items()}
    return out


def inspect_queue(resolve, project_id=None, details=False):
    _, s = snapshot(resolve, project_id)
    return {'status': 'observed', **public_snapshot(s, details)}


def status_once(resolve, project_id, job_id, details=False):
    jid = text(job_id, 'job ID')
    project, pid, _ = target(resolve, project_id)
    rows = jobs(project)
    matches = [r for r in rows if r['JobId'] == jid]
    if not matches:
        return {'status': 'not_found', 'project_id': pid, 'job_id': jid,
                'file_verified': False, 'repeat_mutation': False}
    state = classify_status(read(project, 'GetRenderJobStatus', jid))
    active = boolean(read(project, 'IsRenderingInProgress'), 'IsRenderingInProgress')
    after = [r for r in jobs(project) if r['JobId'] == jid]
    target(resolve, pid)
    if queue_structure(after) != queue_structure(matches):
        raise PreflightError('Selected job changed or disappeared during status read')
    result = {'status': 'observed', 'project_id': pid, 'job_id': jid,
              'is_rendering': active, **{k: v for k, v in state.items() if k != 'raw'}}
    if details:
        result['raw_status'] = state['raw']
        result['raw_job'] = matches[0]
    return result


def bounds(timeout, interval, max_polls):
    if type(timeout) not in (int, float) or not math.isfinite(timeout) or not 0 < timeout <= 86400:
        raise PreflightError('timeout must be finite, > 0 and <= 86400 seconds')
    if type(interval) not in (int, float) or not math.isfinite(interval) or not 0.1 <= interval <= 60:
        raise PreflightError('interval must be finite and 0.1..60 seconds')
    if type(max_polls) is not int or not 1 <= max_polls <= MAX_POLLS:
        raise PreflightError('max_polls must be an integer in 1..10000')


def wait_job(resolve, project_id, job_id, timeout=120, interval=1, max_polls=600,
             details=False, clock=time.monotonic, sleep=time.sleep):
    """Read-only soft deadline, including Ready. It never stops/restarts Resolve."""
    text(project_id, 'project ID')
    text(job_id, 'job ID')
    bounds(timeout, interval, max_polls)
    begin = clock()
    deadline = begin + timeout
    last = None
    samples = 0
    def result(outcome):
        return {'status': outcome, 'project_id': project_id, 'job_id': job_id,
                'samples': samples, 'elapsed_seconds': max(0, clock() - begin),
                'last_observation': last, 'file_verified': False,
                'render_stopped_by_script': False, 'repeat_mutation': False,
                'timeout_kind': 'soft; cannot interrupt a blocked native call'}
    try:
        while samples < max_polls:
            if clock() >= deadline:
                return result('timeout_unknown')
            last = status_once(resolve, project_id, job_id, details)
            samples += 1
            # A native call may have returned after the deadline; retain evidence,
            # but do not claim a within-budget successful wait.
            if clock() >= deadline:
                return result('timeout_unknown')
            if last['status'] == 'not_found':
                return result('job_missing_unknown')
            category = last['category']
            if category == 'complete':
                return result('job_complete_file_unverified')
            if category in ('failed', 'cancelled'):
                return result('job_' + category)
            if category in ('unknown', 'external'):
                return result('state_unknown' if category == 'unknown' else 'external_mode_not_supported')
            sleep(min(interval, max(0, deadline - clock())))
    except KeyboardInterrupt:
        return result('observer_interrupted_render_unchanged')
    except Exception as exc:
        out = result('observation_failed_unknown')
        out['error_type'] = type(exc).__name__
        return out
    return result('poll_limit_unknown')


def selected_ids(ids, allow_empty=False):
    if not isinstance(ids, list) or len(ids) > MAX_DELETE or (not ids and not allow_empty):
        raise PreflightError('Supply 1..25 explicit job IDs; no wildcard or implicit all')
    values = [text(x, 'job ID') for x in ids]
    if len(set(values)) != len(values):
        raise PreflightError('Duplicate selected job IDs')
    return values


def local_scope(s, allow_running=False):
    for state in s['statuses'].values():
        if not isinstance(state['job_status'], str) or state['job_status'] not in LOCAL_STATES or state['issues']:
            raise PreflightError('Unknown or background/remote state: mutation refused')
    rendering = [k for k, v in s['statuses'].items() if v['category'] == 'rendering']
    if not allow_running and (s['is_rendering'] or rendering):
        raise PreflightError('Queue must be idle; this script will not stop it first')
    if allow_running and bool(rendering) != s['is_rendering']:
        raise PreflightError('Global rendering flag and job states disagree')
    return rendering


def plan_action(resolve, action, project_id=None, job_ids=None, timeline_id=None):
    if action not in ('add', 'start', 'delete', 'cleanup', 'stop'):
        raise PreflightError('Unknown action')
    ids = selected_ids(job_ids or [], allow_empty=action in ('add', 'stop'))
    if action == 'add' and ids:
        raise PreflightError('AddRenderJob does not accept an existing job ID')
    if action in ('start', 'delete') and len(ids) != 1:
        raise PreflightError('Exactly one selected job is required')
    project, s = snapshot(resolve, project_id, timeline_id, action == 'add')
    rendering = local_scope(s, allow_running=action == 'stop')
    known = {r['JobId'] for r in s['jobs']}
    if any(jid not in known for jid in ids):
        raise PreflightError('A selected ID is not present in this project queue')
    if action == 'start' and s['statuses'][ids[0]]['job_status'] != 'Ready':
        raise PreflightError('This tutorial starts only an existing local Ready job; no restart')
    if action == 'cleanup' and any(s['statuses'][jid]['category'] not in ('complete', 'failed', 'cancelled') for jid in ids):
        raise PreflightError('Cleanup accepts selected terminal jobs only; use single-delete for Ready')
    if action == 'stop' and ids and set(ids) != set(rendering):
        raise PreflightError('StopRendering is not job-scoped; supplied IDs must cover every observed Rendering job')
    plan = {'action': action, 'project_id': s['project_id'], 'timeline_id': s['timeline_id'],
            'selected_job_ids': ids, 'observed_rendering_ids': rendering,
            'scope': 'project-wide stop' if action == 'stop' else 'explicit IDs' if ids else 'one new job',
            'observed_state_hash': digest(stable(s)),
            'settings_fully_read_back': False, 'atomic': False}
    plan['plan_hash'] = digest(plan)
    return project, s, plan


def mutate(resolve, action, project_id=None, job_ids=None, timeline_id=None,
           apply=False, expect_plan=None, test_copy=False, settings_reviewed=False,
           allow_project_stop=False, details=False):
    project, before, plan = plan_action(resolve, action, project_id, job_ids, timeline_id)
    report = {'status': 'plan', 'plan': plan, 'before': public_snapshot(before, details),
              'mutations_sent': 0, 'verified_deleted_ids': [], 'file_verified': False,
              'retry_allowed': False}
    if not apply:
        return report
    text(project_id, '--project-id required for apply')
    if not test_copy or not expect_plan or expect_plan != plan['plan_hash']:
        raise PreflightError('Apply needs --test-copy and the exact current --expect-plan')
    if action == 'add':
        text(timeline_id, '--timeline-id required for AddRenderJob apply')
    if action in ('add', 'start') and not settings_reviewed:
        raise PreflightError('Review Deliver/job settings and acknowledge --settings-reviewed')
    if action == 'stop' and not allow_project_stop:
        raise PreflightError('Stop is project-wide; explicit --allow-project-stop required')
    method = {'add': 'AddRenderJob', 'start': 'StartRendering', 'stop': 'StopRendering',
              'delete': 'DeleteRenderJob', 'cleanup': 'DeleteRenderJob'}[action]
    # Resolve callable BEFORE any sent marker; the same capability is reused.
    fn = getattr(project, method, None)
    if not callable(fn):
        raise PreflightError('Missing mutating method: ' + method)
    _, current, current_plan = plan_action(resolve, action, project_id, job_ids, timeline_id)
    if current_plan != plan:
        raise PreflightError('Plan changed immediately before mutation; no call sent')
    if action == 'stop' and not current['is_rendering']:
        report['status'] = 'already_idle_no_call'
        return report
    try:
        if action in ('delete', 'cleanup'):
            expected = stable(current)
            for jid in plan['selected_job_ids']:
                _, now = snapshot(resolve, project_id)
                local_scope(now)
                if stable(now) != expected:
                    report['status'] = 'partial_or_changed_before_next_call'
                    return report
                report['attempted_job_id'] = jid
                report['mutations_sent'] += 1
                returned = fn(jid)  # One attempt per explicitly selected ID.
                _, after = snapshot(resolve, project_id)
                expected['jobs'] = [r for r in expected['jobs'] if r['JobId'] != jid]
                expected['statuses'].pop(jid, None)
                report['after'] = public_snapshot(after, details)
                if returned is not True or stable(after) != expected:
                    report['status'] = 'mutation_outcome_unknown'
                    report['return_type'] = type(returned).__name__
                    return report
                report['verified_deleted_ids'].append(jid)
            report['status'] = 'selected_deletions_verified'
            return report
        report['mutations_sent'] = 1
        returned = fn() if action in ('add', 'stop') else fn([plan['selected_job_ids'][0]], False)
        report['return_type'] = type(returned).__name__
        if action == 'add' and isinstance(returned, str) and returned.strip():
            report['returned_job_id'] = returned  # Preserve before post-call read may fail.
        _, after = snapshot(resolve, project_id, timeline_id, action == 'add')
        report['after'] = public_snapshot(after, details)
        if action == 'add':
            jid = text(returned, 'AddRenderJob returned ID')
            old_ids = [r['JobId'] for r in before['jobs']]
            new_ids = [r['JobId'] for r in after['jobs'] if r['JobId'] not in old_ids]
            retained = copy.deepcopy(after)
            retained['jobs'] = [r for r in retained['jobs'] if r['JobId'] != jid]
            retained['statuses'].pop(jid, None)
            if jid in old_ids or new_ids != [jid] or stable(retained) != stable(before):
                report['status'] = 'mutation_outcome_unknown'
            else:
                report['status'] = 'job_added_needs_settings_review'
        elif action == 'start':
            # A True accepts a launch request, not proof of output completion.
            jid = plan['selected_job_ids'][0]
            unchanged = queue_structure(before['jobs']) == queue_structure(after['jobs'])
            others_ok = all(after['statuses'].get(k, {}).get('job_status') == v['job_status']
                            for k, v in before['statuses'].items() if k != jid)
            report['status'] = ('start_accepted_needs_observation' if returned is True and unchanged and others_ok
                                else 'mutation_outcome_unknown')
        else:
            # StopRendering has no documented bool success contract; None is valid.
            unchanged = queue_structure(before['jobs']) == queue_structure(after['jobs'])
            rendering = any(v['category'] == 'rendering' for v in after['statuses'].values())
            unknown = any(v['category'] in ('unknown', 'external') for v in after['statuses'].values())
            report['status'] = ('idle_observed_after_stop' if unchanged and not after['is_rendering'] and not rendering and not unknown
                                else 'stop_sent_needs_observation')
            report['rolled_back'] = False
        return report
    except (Exception, KeyboardInterrupt) as exc:
        report['status'] = 'mutation_outcome_unknown'
        report['error_type'] = type(exc).__name__
        return report  # Never retry, reverse or delete a newly added job here.


def cli_target(parser, timeline=False):
    parser.add_argument('--project-id', help='Exact opaque ID; required for apply/wait')
    if timeline:
        parser.add_argument('--timeline-id', help='Exact current timeline ID for AddRenderJob')
    parser.add_argument('--details', action='store_true', help='Include local names/paths/errors; redact before sharing')


def cli_write(parser, action):
    cli_target(parser, timeline=action == 'add')
    if action != 'add':
        parser.add_argument('--job-id', action='append', default=[], required=action != 'stop',
                            help='Exact ID; repeat only for cleanup or project-stop scope check')
    parser.add_argument('--apply', action='store_true')
    parser.add_argument('--expect-plan', help='SHA-256 of the reviewed current plan')
    parser.add_argument('--test-copy', action='store_true', help='Acknowledge isolated test project and sole operator')
    if action in ('add', 'start'):
        parser.add_argument('--settings-reviewed', action='store_true', help='Acknowledge manual review; not a technical verification')
    if action == 'stop':
        parser.add_argument('--allow-project-stop', action='store_true', help='Acknowledge non-job-scoped stop')


def write_from_args(resolve, action, args):
    return mutate(resolve, action, project_id=args.project_id, job_ids=getattr(args, 'job_id', []),
                  timeline_id=getattr(args, 'timeline_id', None), apply=args.apply,
                  expect_plan=args.expect_plan, test_copy=args.test_copy,
                  settings_reviewed=getattr(args, 'settings_reviewed', False),
                  allow_project_stop=getattr(args, 'allow_project_stop', False), details=args.details)


def print_result(work):
    try:
        out = work()
    except PreflightError as exc:
        out = {'status': 'preflight_refused', 'error': str(exc), 'mutations_sent': 0}
    except KeyboardInterrupt:
        out = {'status': 'observer_interrupted', 'render_stopped_by_script': False}
    except Exception as exc:
        out = {'status': 'read_or_connection_failed', 'error_type': type(exc).__name__}
    print(json.dumps(out, ensure_ascii=False, allow_nan=False, indent=2))
    if out['status'] in ('plan', 'observed', 'selected_deletions_verified', 'already_idle_no_call',
                         'idle_observed_after_stop', 'job_complete_file_unverified'):
        return 0
    return 2 if out['status'] == 'preflight_refused' else 3

Общий helper подключения — посмотреть и скачать

Этот файл находит и загружает установленный мост. При импорте Python может записывать кэш байткода, а нативный вызов — зависнуть; это не песочница и не механизм тайм-аута. Используйте доверенную локальную установку.

resolve_api_connection.py
Скачать .py
"""Load the installed Blackmagic module; do not install a similarly named package.

This helper uses standard desktop installation paths or RESOLVE_SCRIPT_API.
It does not enable scripting, start Resolve, open projects or retry API calls.
Validate Python/native-library compatibility against your installed README.txt.
"""
from __future__ import annotations

import importlib.util
import os
from pathlib import Path
import struct
import sys


class ResolveConnectionError(RuntimeError):
    """An explicit environment, module, or application-connection failure."""


def scripting_root() -> Path:
    configured = os.environ.get("RESOLVE_SCRIPT_API")
    if configured is not None:
        if not configured.strip() or configured != configured.strip():
            raise ResolveConnectionError("RESOLVE_SCRIPT_API is empty or has outer whitespace")
        if configured.startswith(('"', "'")) or configured.endswith(('"', "'")):
            raise ResolveConnectionError("Do not store literal outer quotes in RESOLVE_SCRIPT_API")
        root = Path(os.path.expandvars(configured)).expanduser()
    elif sys.platform == "win32":
        root = Path(os.environ.get("PROGRAMDATA", r"C:\ProgramData")) / "Blackmagic Design/DaVinci Resolve/Support/Developer/Scripting"
    elif sys.platform == "darwin":
        root = Path("/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting")
    elif sys.platform.startswith("linux"):
        root = Path("/opt/resolve/Developer/Scripting")
    else:
        raise ResolveConnectionError("Set RESOLVE_SCRIPT_API for this platform/installation")
    if not root.is_absolute():
        raise ResolveConnectionError("RESOLVE_SCRIPT_API must be an absolute Scripting directory")
    return root


def module_file() -> Path:
    # RESOLVE_SCRIPT_API points to Scripting, not its Modules subdirectory.
    return scripting_root() / "Modules" / "DaVinciResolveScript.py"


def load_resolve_module():
    if struct.calcsize("P") * 8 != 64:
        raise ResolveConnectionError("Use a compatible 64-bit Python interpreter")
    source = module_file()
    if not source.is_file():
        raise ResolveConnectionError("Official DaVinciResolveScript.py not found at: " + str(source))
    configured_lib = os.environ.get("RESOLVE_SCRIPT_LIB")
    if configured_lib is not None:
        lib = Path(os.path.expandvars(configured_lib)).expanduser()
        if not lib.is_absolute() or not lib.is_file():
            raise ResolveConnectionError("RESOLVE_SCRIPT_LIB must name the installed native library file")
    name = "DaVinciResolveScript"
    spec = importlib.util.spec_from_file_location(name, source)
    if spec is None or spec.loader is None:
        raise ResolveConnectionError("Cannot create a loader for the official module")
    previous = sys.modules.get(name)
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    try:
        spec.loader.exec_module(module)
        # Blackmagic's wrapper can replace its sys.modules entry with a native module.
        loaded = sys.modules[name]
        if not callable(getattr(loaded, "scriptapp", None)):
            raise ResolveConnectionError("Loaded module does not expose scriptapp")
        return loaded
    except Exception as exc:
        if previous is None:
            sys.modules.pop(name, None)
        else:
            sys.modules[name] = previous
        raise ResolveConnectionError("Official module was found, but loading it failed") from exc


def connect():
    module = load_resolve_module()
    try:
        resolve = module.scriptapp("Resolve")
    except Exception as exc:
        raise ResolveConnectionError("scriptapp failed; inspect the original exception") from exc
    if not resolve:
        raise ResolveConnectionError("No Resolve object: check running Studio, Local access and Python environment")
    return resolve


def project_manager(resolve):
    manager = resolve.GetProjectManager()
    if not manager:
        raise ResolveConnectionError("Resolve connected, but Project Manager is unavailable")
    return manager

Команды запуска

Прочитать очередь

Прочитать очередь
python inspect_render_queue.py

Сначала получаем проект и ID без скрытого запуска.

Подготовить один запуск

Подготовить один запуск
python start_single_render_job.py --project-id "PROJECT_ID" --job-id "JOB_ID"

Только план. Дальнейшее применение разобрано в статье об одном задании.

Что делает код

  1. Читать, а не угадывать

    GetRenderJobList и GetRenderJobStatus остаются разными наблюдениями. Отсутствующее поле, неверный ответ, повтор ID и пустая очередь имеют разный смысл.

  2. Сохранить выбранную область

    Основной скрипт вызывает скачиваемый общий helper ниже. Проверки — политика учебного примера, не песочница и не новая система прав Resolve.

  3. Правильно прочитать результат

    Код 0 означает завершённое наблюдение, план или узко проверенную операцию. Код 2 — отказ предварительной проверки. Код 3 — ошибка, неизвестность или необходимость сверки; это не разрешение повторить вызов.

Границы, которые важно сохранить

С --details отчёт может раскрыть имена клиентов, пути и ошибки. ID тоже могут идентифицировать работу: проверьте весь отчёт перед публикацией; автоматического обезличивания нет.

Нет смены пресета, создания папок, запуска всей очереди, автоматических повторов, отката и сохранения проекта. Нативные вызовы могут зависнуть. Bool и совпавшие чтения не гарантируют состояние всего проекта.

Complete в API — не сертификат качества файла. Отдельно проверьте ожидаемые файлы, декодирование, длительность, звук, размеры и требования сдачи.

DEBUG / OBSERVATION FIRST

Ошибка — отправная точка, не готовый диагноз.

Отказ до вызова

Возможная причинаНе совпали точный ID, план, структура очереди или требуемый метод.

Следующая проверкаПрочитайте отчёт и интерфейс своей установки. Не подбирайте псевдонимы методов и не превращайте непрозрачный ID в число.

Неизвестный исход после отправки

Возможная причинаИзменяющий вызов либо последующее чтение завершилось ошибкой; возможно параллельное изменение очереди.

Следующая проверкаСохраните возвращённый ID и наблюдения. Сначала сверьте Resolve; не повторяйте операцию вслепую.

Фоновый или удалённый режим

Возможная причинаОграниченный изменяющий пример не может установить локальную исключительную область для такого состояния.

Следующая проверкаПрочитайте исходное состояние с --details и используйте отдельно проверенный процесс. Не переименовывайте его в Ready ради обхода проверки.

PRACTICE / YOUR OWN EVIDENCE

Проверьте на своей тестовой копии

  1. Откройте отдельный проект с двумя различимыми заданиями и получите инвентаризацию.
  2. Сопоставьте оба ID и поля с интерфейсом; подготовьте план запуска только одного задания.
  3. Примените отдельный скрипт запуска, затем наблюдайте его ID и вручную проверьте выходной файл.
КРИТЕРИЙ ПРИЁМКИ

Второе задание не запущено и не удалено. Инвентаризация ничего не изменила; статус первого и проверка файла зафиксированы раздельно.

Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.

FAQ

Вопросы, которые возникают на практике.

Почему здесь не один скрипт от пресета до рендера?

Настройки, создание задания, запуск и приёмка имеют разные последствия и условия. Разделение позволяет остановиться до следующей записи. Сквозной сценарий выделен в отдельную заключительную группу.[18]

Нужен ли активный таймлайн для чтения очереди?

Нет, методы очереди вызываются у Project. Текущий таймлайн понадобится для создания нового задания, но не для чтения существующей записи по ID.[18]

Можно ли считать is_rendering=False успехом?

Нет. Приложение может быть свободно потому, что задание не запущено, завершилось ошибкой или отменено. Нужен статус выбранного ID и отдельная проверка файла.[18]

SOURCES / VERSION BOUNDARIES

Источники, а не подразумеваемая сертификация.

Сверка источников, офлайн-тесты Python и проверка интерфейса — разные виды подтверждения. Ни один из них не означает, что каждый пример проверен со всеми сборками Studio, ОС и версиями Python. Указанная дата — дата редакционной проверки, не сертификат живого прогона.

  1. Blackmagic Design — DaVinci Resolve Studio 21.1 release notes

    8 сентября 2026. В объявлении выпуска Python scripting отнесён к Studio. Это источник требований редакции, а не протокол испытания наших примеров.

  2. Blackmagic Design — Support Center

    Каталог обновлений проверен 5 октября 2026: опубликована ветка 21.1.1. Последний выпуск не равен минимальной версии каждого метода.

  3. Blackmagic Scripting README — snapshot 24 July 2026 (Resolve 21.0.4)

    Публичная копия первичного README Blackmagic, поддерживаемая X-Raym. По ней сверены базовые сигнатуры; это не официальный хостинг и не README 21.1.1. Оригинал своей установки: Help → Documentation → Developer → Scripting → README.txt; расположение пункта зависит от версии.

  4. Blackmagic Design — Scripting and Automation

    Официальное описание Python/Lua scripting и интеграций Studio. Нельзя переносить обещания Studio на Free без проверки конкретного выпуска.

  5. Blackmagic DaVinciResolveScript.pyi — RenderJobInfo, RenderJobStatus and queue methods

    Для S07 6 октября 2026 проверены опубликованная копия первичного интерфейса Blackmagic, поля JobId/JobStatus и методы очереди. Хостинг сторонний; сверьте .pyi своей установки. Контракт остановки не принимает ID и не объявляет bool успеха. Это проверка документа, не живая сертификация.

  6. Python — time.monotonic and time.sleep

    Монотонная шкала подходит для измерения прошедшего времени. Пауза и проверка часов не прерывают заблокированный нативный вызов; в S07 реализован мягкий deadline и независимый предел опросов.

KEEP BUILDING

Продолжайте с конкретной задачей.

API / Render

Как очистить Render Queue без удаления чужих заданий

Выборочная очистка завершённых, ошибочных и отменённых Render Jobs по явным ID. Предварительный план, проверка соседних записей и остановка при частичном исходе.

API / Render

Как создать Render Job через Python API

Один AddRenderJob из вручную проверенных настроек Deliver: предварительный план, точный таймлайн, сохранение JobId и проверка изменения очереди без автоматического запуска.

API / Render

Render Settings: точные ключи и проверяемый запрос

Подготовка SetRenderSettings через ограниченный JSON: правильные ключи, строгие типы, существующая папка и просмотр плана. Без неизвестных полей, создания каталогов, скрытой смены кодека и запуска рендера.

Вернуться в библиотеку API →MCP и AI-ассистенты →