{"record":{"id":"d176ec63c611990e","repo":"remotion-dev/remotion","slug":"no-keyframe-changes-were-specified","errorCode":null,"errorMessage":"No keyframe changes were specified","messagePattern":"No keyframe changes were specified","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/browser-studio/src/browser-studio-operations.ts","lineNumber":513,"sourceCode":"\t\t\tonProjectChange(project);\n\t\t\trefreshDefaultPropsSubscriptions();\n\t\t\tif (!metadata.skipSequencePropsUpdate) {\n\t\t\t\trefreshSequencePropsSubscriptions();\n\t\t\t}\n\t\t},\n\t});\n\n\tconst mutateKeyframesInProject = async ({\n\t\tproject,\n\t\tsequenceMutations,\n\t\teffectMutations,\n\t}: {\n\t\tproject: VirtualProject;\n\t\tsequenceMutations: SequenceKeyframeMutation[];\n\t\teffectMutations: EffectKeyframeMutation[];\n\t}) => {\n\t\tif (sequenceMutations.length === 0 && effectMutations.length === 0) {\n\t\t\tthrow new Error('No keyframe changes were specified');\n\t\t}\n\n\t\tconst mergeUpdates = <T extends SequenceKeyframeUpdate>(updates: T[]) => {\n\t\t\tconst merged: T[] = [];\n\t\t\tfor (const update of updates) {\n\t\t\t\tconst existingMove = merged.find(\n\t\t\t\t\t(candidate) =>\n\t\t\t\t\t\tcandidate.key === update.key &&\n\t\t\t\t\t\tcandidate.operation.type === 'move' &&\n\t\t\t\t\t\tupdate.operation.type === 'move',\n\t\t\t\t);\n\t\t\t\tif (\n\t\t\t\t\texistingMove?.operation.type === 'move' &&\n\t\t\t\t\tupdate.operation.type === 'move'\n\t\t\t\t) {\n\t\t\t\t\texistingMove.operation.moves.push(...update.operation.moves);\n\t\t\t\t} else {\n\t\t\t\t\tmerged.push(update);","sourceCodeStart":495,"sourceCodeEnd":531,"githubUrl":"https://github.com/remotion-dev/remotion/blob/b2f4e34732f3c6c222ea029413e22dc402218cd7/packages/browser-studio/src/browser-studio-operations.ts#L495-L531","documentation":"mutateKeyframesInProject powers the sequence and effect keyframe operations (move/add/delete keyframes). It refuses no-op calls: when both sequenceMutations and effectMutations are empty arrays it throws instead of committing an empty change and polluting the undo stack.","triggerScenarios":"Invoking a keyframe mutation operation (via commitKeyframeMutations consumers such as updateSequenceKeyframes-style flows) after client-side filtering removed every requested change, leaving zero mutations in both arrays.","commonSituations":"Drag handlers that end without an effective change; batching logic that de-duplicates updates and accidentally empties the batch; gesture code that always calls save even when nothing moved.","solutions":["Skip the operation call when both mutation arrays are empty — treat it as a no-op.","Fix the upstream filtering so intended keyframe changes survive into the request.","Keep UI state unchanged on empty batches instead of surfacing the error to the user."],"exampleFix":"// before\nawait saveKeyframes({sequenceMutations: filtered, effectMutations: []});\n\n// after\nif (filtered.length > 0) {\n  await saveKeyframes({sequenceMutations: filtered, effectMutations: []});\n}","handlingStrategy":"validation","validationCode":"const hasChanges =\n  sequenceMutations.length > 0 || effectMutations.length > 0;\nif (hasChanges) {\n  await commitKeyframes({sequenceMutations, effectMutations});\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Short-circuit gesture end handlers when nothing effectively changed.","Keep a dirty flag and only call keyframe save APIs when at least one edit is pending.","Unit-test batching logic with de-duplicated inputs to ensure the batch is never emptied."],"tags":["keyframes","validation","no-op"],"backgroundTag":"empty-request-rejected","analyzedSha":"b2f4e34732f3c6c222ea029413e22dc402218cd7","analyzedAt":"2026-08-22T21:45:17.748Z","contentChangedAt":"2026-08-22T21:45:17.748Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}