01 / BEFORE THE CODE
До кода — условия запуска.
Среда и источник
Внешний Python 3.9+ и запущенный Resolve Studio с локальным scripting. Сверьте интерфейс своей установленной версии. В этом проходе проверена опубликованная копия интерфейса Blackmagic на 6 октября 2026; живая совместимость, минимальная версия и работа в Free не заявляются.
Точная цель
Используйте отдельную тестовую копию и одного оператора. ID проекта и задания — точные непрозрачные строки, не номер очереди, имя таймлайна или файла. Текущий таймлайн нужен только для создания нового задания; чтению и управлению существующими заданиями он не требуется.
Перед изменением
Сначала прочитайте план. Применение требует точной цели, совпавшего хеша и подтверждения тестовой копии. Для add/start самостоятельно проверьте Deliver и выбранное задание. Хеш не является блокировкой, проверкой прав или полным снимком настроек.
Ориентируйтесь на актуальные требования Studio и документацию установленной версии, а не на старые инструкции для консоли или Free.[1][4][3]
02 / API CONTRACT
Какой объект. Какой метод. Какой результат.
GetRenderJobList()- Вход
- Без аргументов.
- Возвращает
- Список RenderJobInfo.
- Проверить
- Пустой список допустим; None и неизвестная форма — не пустая очередь.
StartRendering([job_id], False)- Вход
- Один точный строковый ID в списке.
- Возвращает
- Bool принятия запуска.
- Проверить
- В этом примере инвентаризации не вызывается; см. отдельный запуск.
GetRenderJobStatus(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% и свободное приложение не доказывают завершение.
Поля меняют пример, не ваш проект. Автосохранения и отправки нет. Скачанный JSON — отредактированная учебная модель, не журнал Resolve, сертификат завершения или разрешение выполнить команду.
PYTHON / COMPLETE EXAMPLE
Прочитайте код. Затем запускайте осознанно.
Положите inspect_render_queue.py, resolve_api_queue.py и resolve_api_connection.py в одну папку. Комплект содержит восемь основных скриптов, оба helper и инструкции RU/EN. Официальный SDK не включён; используется мост установленного Blackmagic.
Скачать все примеры с инструкцией (.zip) ↓"""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 не прерывает зависший нативный вызов.
"""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 может записывать кэш байткода, а нативный вызов — зависнуть; это не песочница и не механизм тайм-аута. Используйте доверенную локальную установку.
"""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"Только план. Дальнейшее применение разобрано в статье об одном задании.
Что делает код
Читать, а не угадывать
GetRenderJobList и GetRenderJobStatus остаются разными наблюдениями. Отсутствующее поле, неверный ответ, повтор ID и пустая очередь имеют разный смысл.
Сохранить выбранную область
Основной скрипт вызывает скачиваемый общий helper ниже. Проверки — политика учебного примера, не песочница и не новая система прав Resolve.
Правильно прочитать результат
Код 0 означает завершённое наблюдение, план или узко проверенную операцию. Код 2 — отказ предварительной проверки. Код 3 — ошибка, неизвестность или необходимость сверки; это не разрешение повторить вызов.
С --details отчёт может раскрыть имена клиентов, пути и ошибки. ID тоже могут идентифицировать работу: проверьте весь отчёт перед публикацией; автоматического обезличивания нет.
Нет смены пресета, создания папок, запуска всей очереди, автоматических повторов, отката и сохранения проекта. Нативные вызовы могут зависнуть. Bool и совпавшие чтения не гарантируют состояние всего проекта.
Complete в API — не сертификат качества файла. Отдельно проверьте ожидаемые файлы, декодирование, длительность, звук, размеры и требования сдачи.
DEBUG / OBSERVATION FIRST
Ошибка — отправная точка, не готовый диагноз.
Отказ до вызова
Возможная причинаНе совпали точный ID, план, структура очереди или требуемый метод.
Следующая проверкаПрочитайте отчёт и интерфейс своей установки. Не подбирайте псевдонимы методов и не превращайте непрозрачный ID в число.
Неизвестный исход после отправки
Возможная причинаИзменяющий вызов либо последующее чтение завершилось ошибкой; возможно параллельное изменение очереди.
Следующая проверкаСохраните возвращённый ID и наблюдения. Сначала сверьте Resolve; не повторяйте операцию вслепую.
Фоновый или удалённый режим
Возможная причинаОграниченный изменяющий пример не может установить локальную исключительную область для такого состояния.
Следующая проверкаПрочитайте исходное состояние с --details и используйте отдельно проверенный процесс. Не переименовывайте его в Ready ради обхода проверки.
PRACTICE / YOUR OWN EVIDENCE
Проверьте на своей тестовой копии
- Откройте отдельный проект с двумя различимыми заданиями и получите инвентаризацию.
- Сопоставьте оба ID и поля с интерфейсом; подготовьте план запуска только одного задания.
- Примените отдельный скрипт запуска, затем наблюдайте его ID и вручную проверьте выходной файл.
Второе задание не запущено и не удалено. Инвентаризация ничего не изменила; статус первого и проверка файла зафиксированы раздельно.
Это условия вашей проверки, а не отчёт об испытании, уже выполненном на сайте.
FAQ
Вопросы, которые возникают на практике.
Почему здесь не один скрипт от пресета до рендера?
Настройки, создание задания, запуск и приёмка имеют разные последствия и условия. Разделение позволяет остановиться до следующей записи. Сквозной сценарий выделен в отдельную заключительную группу.[18]
Нужен ли активный таймлайн для чтения очереди?
Нет, методы очереди вызываются у Project. Текущий таймлайн понадобится для создания нового задания, но не для чтения существующей записи по ID.[18]
Можно ли считать is_rendering=False успехом?
Нет. Приложение может быть свободно потому, что задание не запущено, завершилось ошибкой или отменено. Нужен статус выбранного ID и отдельная проверка файла.[18]
SOURCES / VERSION BOUNDARIES
Источники, а не подразумеваемая сертификация.
Сверка источников, офлайн-тесты Python и проверка интерфейса — разные виды подтверждения. Ни один из них не означает, что каждый пример проверен со всеми сборками Studio, ОС и версиями Python. Указанная дата — дата редакционной проверки, не сертификат живого прогона.
- Blackmagic Design — DaVinci Resolve Studio 21.1 release notes
8 сентября 2026. В объявлении выпуска Python scripting отнесён к Studio. Это источник требований редакции, а не протокол испытания наших примеров.
- Blackmagic Design — Support Center
Каталог обновлений проверен 5 октября 2026: опубликована ветка 21.1.1. Последний выпуск не равен минимальной версии каждого метода.
- 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; расположение пункта зависит от версии.
- Blackmagic Design — Scripting and Automation
Официальное описание Python/Lua scripting и интеграций Studio. Нельзя переносить обещания Studio на Free без проверки конкретного выпуска.
- Blackmagic DaVinciResolveScript.pyi — RenderJobInfo, RenderJobStatus and queue methods
Для S07 6 октября 2026 проверены опубликованная копия первичного интерфейса Blackmagic, поля JobId/JobStatus и методы очереди. Хостинг сторонний; сверьте .pyi своей установки. Контракт остановки не принимает ID и не объявляет bool успеха. Это проверка документа, не живая сертификация.
- Python — time.monotonic and time.sleep
Монотонная шкала подходит для измерения прошедшего времени. Пауза и проверка часов не прерывают заблокированный нативный вызов; в S07 реализован мягкий deadline и независимый предел опросов.