Version 1.85 changes what several calls do. Your code keeps compiling, so read the sections for the calls that your code makes, and check them after you upgrade.
Video Export Frame Rate#
Before v1.85, engine.block.exportVideo() without a framerate option exported at 30 frames per second. From v1.85 it uses the scene’s scene/frameRate property. The export then matches the preview, including its motion blur. An explicit framerate still wins.
| Before v1.85 | From v1.85 | |
|---|---|---|
No framerate option |
30 fps | The scene’s scene/frameRate |
framerate option |
That value | That value |
New scenes start with scene/frameRate set to 30, so their exports do not change.
Do I Need to Migrate?#
You are affected if both are true:
- You export video without a
framerateoption. - Your scenes have a
scene/frameRateother than 30. A Lottie import with@imgly/lottie-importersets it to the frame rate of the animation, and your own code or templates can set it too.
Read the frame rate of a scene to check:
const scene = engine.scene.get()!;console.log(engine.block.getFloat(scene, 'scene/frameRate'));Keep the Previous Output#
Pass framerate: 30 to export at 30 frames per second, as before v1.85.
Before#
const video = await engine.block.exportVideo(page, { mimeType: 'video/mp4' });Now#
const video = await engine.block.exportVideo(page, { mimeType: 'video/mp4', framerate: 30});To export at the scene’s frame rate, leave out framerate.
Video Frame Rate Range#
From v1.85, a video export fails unless its frame rate is greater than 0 and at most 240 frames per second. Before v1.85, a frame rate of 0, a negative frame rate or NaN gave an undefined frame count, and a frame rate above 240 exported a very large number of frames.
You are affected if your code computes the framerate option or reads it from user input. Pass a value in that range, or leave out framerate to use the scene’s frame rate.
MP4 Cover Image#
From v1.85, an MP4 export contains a PNG cover image. By default, the cover shows the page at the playback position when the export starts. The cover keeps the aspect ratio of the video and fits within 512 × 512 pixels. The export has no option to leave out the cover.
You are affected if your code compares the bytes of exported MP4 files, or if the cover must show a specific frame. Set coverTimeOffset to the time of that frame, in seconds from the start of the exported video:
const video = await engine.block.exportVideo(page, { mimeType: 'video/mp4', coverTimeOffset: 0});Export Timeout for Pending Resources#
From v1.85, an export fails after 60 seconds when a block that it exports waits for a resource that makes no loading progress. Before v1.85, such an export never ended. The error has the code ENCODE.BLOCKS_STAYED_PENDING and names the blocks. After this error, remove the blocks or dispose the engine.
A slow resource can also reach this limit, because two steps report no progress while they run: an asynchronous URI resolver and the demux of a video. A block that you set to pending with setState() does not count toward the limit.
You are affected if your exports can wait more than 60 seconds for a resource. Set the exportPendingTimeoutSeconds setting to a larger number of seconds, or to 0 to wait without a limit, as before v1.85:
engine.editor.setSettingFloat('exportPendingTimeoutSeconds', 0);Text Ranges Past the End#
From v1.85 the text range functions fail for a range that ends past the end of the text. They throw an engine error with the code BLOCK.TEXT_RANGE_FROM_OUT_OF_RANGE or BLOCK.TEXT_RANGE_TO_OUT_OF_RANGE. Before v1.85, setTextColor(), setTextFontSize() and the other text style setters and getters used the whole text, or the text up to its end, for such a range. The default range, which you get when you leave out from and to, is unchanged.
You are affected if your code passes a to index that can be larger than the length of the text, for example a fixed index or the length of an earlier text.
Limit the range to the current text. The indices count UTF-16 code units, like string.length.
Before#
engine.block.setTextColor(text, { r: 1, g: 0, b: 0, a: 1 }, 0, 20);Now#
const length = engine.block.getString(text, 'text/text').length;engine.block.setTextColor(text, { r: 1, g: 0, b: 0, a: 1 }, 0, Math.min(20, length));Text Cursor Range Past the End#
From v1.85, engine.block.setTextCursorRange() reads a from index past the end of the text as the end of the text. Before v1.85, it read it as the start of the text, so a range past the end on both sides selected the whole text.
You are affected if your code selects the whole text with a range that can start past the end of the text. Pass a range from 0 to the length of the text instead:
const length = engine.block.getString(text, 'text/text').length;engine.block.setTextCursorRange({ from: 0, to: length });Text Variables in Text Ranges#
From v1.85, the text range functions, such as getTextColors(), setTextColor(), getTextRuns() and getTextCursorRange(), count a text variable such as {{name}} by its characters, as in text/text. Before v1.85, a variable counted as one character, so each index after it pointed to the wrong character, and a range over the whole text failed. A range that starts or ends inside a variable covers the whole variable.
You are affected if your code counts a variable as one character. Count the characters of text/text instead.
Before#
// The text is "Hi {{name}}!", and the variable counted as one character.engine.block.setTextColor(text, { r: 1, g: 0, b: 0, a: 1 }, 4, 5);Now#
const index = engine.block.getString(text, 'text/text').indexOf('!');engine.block.setTextColor(text, { r: 1, g: 0, b: 0, a: 1 }, index, index + 1);Animations Belong to One Block#
From v1.85, setInAnimation(), setLoopAnimation() and setOutAnimation() remove the animation from any other block that used it. Before v1.85, the blocks shared the animation, and destroying the block that got it last, or the page of that block, also removed it from the other blocks.
You are affected if your code sets one animation on more than one block. Create one animation for each block.
Before#
const fade = engine.block.createAnimation('fade');engine.block.setInAnimation(first, fade);engine.block.setInAnimation(second, fade);Now#
engine.block.setInAnimation(first, engine.block.createAnimation('fade'));engine.block.setInAnimation(second, engine.block.createAnimation('fade'));Caption Animations#
From v1.85, the captions on one caption track share their animations when you apply an animation from the ly.img.animations asset library, a caption style, or the animation.in, animation.out and animation.loop actions. Applying an animation to one caption applies it to every caption on the track. A caption that you add to the track takes the animations of the captions already there. A caption whose appearance/animation scope is locked keeps its own animations.
These calls also changed:
- An animation that they apply now fits into the length of the block. When an in and an out animation would overlap, both get shorter in proportion.
- Captions no longer get the typewriter, block swipe, spread and merge animations or the writing style. Applying one of them to a caption changes nothing.
- Applying an animation from the
ly.img.animationslibrary to one block fails when itsappearance/animationscope is locked. Applying an animation to the selection skips that block.
You are affected if your code adds captions to a track whose captions have animations. To animate one caption only, call setInAnimation(), setOutAnimation() or setLoopAnimation() on it after you add it to the track:
engine.block.appendChild(captionTrack, caption);engine.block.setInAnimation(caption, engine.block.createAnimation('fade'));Percent Sizes with maintainCrop#
From v1.85, setWidth(), setHeight() and setSize() with maintainCrop read the value in the size mode of the block, and a Percent size, left or top stays Percent. Before v1.85, they read a Percent value as design units, so the block shrank to less than one unit, and its size mode became Absolute.
You are affected if your code passes a frame size in design units with maintainCrop for a block with a Percent size. Convert the size first: multiply it by getWidth() and divide it by getFrameWidth(), or use the height getters for a height.
Before#
engine.block.setWidth(block, 400, true);Now#
const width = (400 * engine.block.getWidth(block)) / engine.block.getFrameWidth(block);engine.block.setWidth(block, width, true);Block Audio Exports#
From v1.85, an audio/mp4 export of an audio block or a video block with engine.block.exportAudio() applies the playback settings of the block. These are its volume, mute state, playback speed, looping and audio fades, the volume and mute state of its page, and the fade of a transition. Before v1.85, the export returned the audio of the source file unchanged. A block that changes none of these settings still exports a copy of the source audio.
The export of a block that changes one of these settings is mixed and encoded as 48 kHz stereo audio, like the export of a page. It fails where the export of a page fails: in a browser without an audio encoder, for a block with no duration, and for a block on no page.
To export the source audio unchanged, reset these settings before the export.
exportAudio() of a block, a track or a group also applies timeOffset now. The export starts timeOffset seconds later in the block. Before v1.85, these exports ignored it, except an audio/mp4 export of an MP4 file. To keep the previous output, leave out timeOffset or pass 0.
URI Resolvers That Throw#
From v1.85, when the function that you pass to engine.editor.setURIResolver() throws or returns no string, the engine logs a warning and loads the URI unchanged. Before v1.85, that image never finished loading.
You are affected if your resolver throws to keep a URI from loading. Return a URI that cannot load instead, such as one with an unknown scheme. The block then gets an Error state.
Before#
engine.editor.setURIResolver((uri, defaultURIResolver) => { if (uri.startsWith('https://blocked.example.com/')) { throw new Error('Blocked'); } return defaultURIResolver(uri);});Now#
engine.editor.setURIResolver((uri, defaultURIResolver) => { if (uri.startsWith('https://blocked.example.com/')) { return `blocked:${uri}`; } return defaultURIResolver(uri);});