Version 1.85 makes ExportVideoOptions.frameRate a Float? and adds two properties to CMYKProfileInfo. Code that reads options.frameRate as a Float, or that creates a CMYKProfileInfo, needs a change. Other code keeps compiling, but several calls now behave differently, so read the sections for the calls that your code makes.
Video Export Frame Rate#
Before v1.85, ExportVideoOptions.frameRate was a Float that defaulted to 30. From v1.85 it is a Float? that defaults to null, and null exports at the scene’s scene/frameRate. The export then matches the preview, including its motion blur. An explicit frameRate still wins.
| Before v1.85 | From v1.85 | |
|---|---|---|
Type of frameRate |
Float |
Float? |
No frameRate argument |
30 fps | The scene’s scene/frameRate |
frameRate argument |
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 one of these is true:
- Your code reads
options.frameRateas aFloat. It is now nullable, so the code no longer compiles. - You export video without a
frameRateargument, and your scenes have ascene/frameRateother than 30. Your own code or templates can set it.
Read the frame rate of a scene to check:
val scene = requireNotNull(engine.scene.get())println(engine.block.getFloat(scene, property = "scene/frameRate"))Read the Frame Rate of the Options#
Fall back to the previous default.
Before#
val framesPerSecond: Float = options.frameRateNow#
val framesPerSecond: Float = options.frameRate ?: 30FKeep the Previous Output#
Pass frameRate = 30F to export at 30 frames per second, as before v1.85.
Before#
val options = ExportVideoOptions()Now#
val options = ExportVideoOptions(frameRate = 30F)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 frameRate 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:
val options = ExportVideoOptions(coverTimeOffset = 0.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.
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(keypath = "exportPendingTimeoutSeconds", value = 0F)Text Ranges Past the End#
From v1.85, the text style setters and getters, such as setTextColor() and setTextFontSize(), throw an EngineException for a range that ends past the end of the text, as on Web and Node.js. Before v1.85, they used the text up to its end. 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, or pass -1 for the whole text. The indices count UTF-16 code units, like String.length.
Before#
val red = Color.fromRGBA(r = 1F, g = 0F, b = 0F)engine.block.setTextColor(text, color = red, from = 0, to = 20)Now#
val red = Color.fromRGBA(r = 1F, g = 0F, b = 0F)val length = engine.block.getString(text, property = "text/text").lengthengine.block.setTextColor(text, color = red, from = 0, to = minOf(20, length))Text Cursor Range Past the End#
From v1.85, engine.block.setTextCursorRange() reads a start 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:
val length = engine.block.getString(text, property = "text/text").lengthengine.block.setTextCursorRange(0..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, color = red, from = 4, to = 5)Now#
val index = engine.block.getString(text, property = "text/text").indexOf('!')engine.block.setTextColor(text, color = red, from = index, to = 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#
val fade = engine.block.createAnimation(AnimationType.Fade)engine.block.setInAnimation(block = first, animation = fade)engine.block.setInAnimation(block = second, animation = fade)Now#
engine.block.setInAnimation(block = first, animation = engine.block.createAnimation(AnimationType.Fade))engine.block.setInAnimation(block = second, animation = engine.block.createAnimation(AnimationType.Fade))Caption Animations#
From v1.85, the captions on one caption track share their animations when an animation comes from the ly.img.animations asset library or a caption style, for example from the editor’s Animations button. 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 animations also changed:
- An animation from the library 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 library to one block fails when its
appearance/animationscope is locked.
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(parent = captionTrack, child = caption)engine.block.setInAnimation(block = caption, animation = engine.block.createAnimation(AnimationType.Fade))Percent Sizes with maintainCrop#
From v1.85, setWidth() and setHeight() 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, value = 400F, maintainCrop = true)Now#
val width = 400F * engine.block.getWidth(block) / engine.block.getFrameWidth(block)engine.block.setWidth(block, value = width, maintainCrop = true)CMYK Profile Info#
From v1.85, CMYKProfileInfo has two new properties: name, the ICC description of the profile, and uri, the URI that the document names the profile by. The engine returns this type and never takes it, so only code that creates one itself, such as a test or a mock, no longer compiles. Pass a value for each new property.
Before#
val info = CMYKProfileInfo(contentHash = hash)Now#
val info = CMYKProfileInfo(contentHash = hash, name = "Coated FOGRA39", uri = "https://example.com/coated-fogra39.icc")