Version 1.85 changes the frame rate of a video export that passes no framerate, makes VideoExportOptions.framerate a Float?, and adds the engine error to the .error case of BlockState. Code that only passes export options keeps compiling. Code that reads options.framerate or matches BlockState.error needs a change. Several other calls now behave differently, so also read the sections for the calls that your code makes.
Video Export Frame Rate#
Before v1.85, VideoExportOptions.framerate was a Float that defaulted to 30. From v1.85 it is a Float? that defaults to nil, and nil 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.framerate. It is now optional, 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:
let scene = try engine.scene.get()!print(try engine.block.getFloat(scene, property: "scene/frameRate"))Read the Frame Rate of the Options#
Unwrap framerate with the previous default.
Before#
let framesPerSecond: Float = options.framerateNow#
let framesPerSecond: Float = options.framerate ?? 30Keep the Previous Output#
Pass framerate: 30 to export at 30 frames per second, as before v1.85.
Before#
let options = VideoExportOptions()Now#
let options = VideoExportOptions(framerate: 30)To export at the scene’s frame rate, leave out framerate.
Block State Errors#
From v1.85 the .error case of BlockState has two associated values: the kind of failure and the engine error behind it. The case is now .error(BlockStateError, engineError: EngineError?). engineError is nil when the engine knows no catalog error, or when your code set the state with setState(_:state:).
You are affected if your code matches the .error case. A pattern with one value no longer compiles. Creating a state with one value, such as .error(.unknown), still compiles, because engineError defaults to nil. == also compares the code of the engine error, so state == .error(.videoDecoding) is false when the engine reports the error behind it. Match the case with a pattern instead, such as if case .error(.videoDecoding, _) = state.
Before#
switch try engine.block.getState(block) {case .error(.videoDecoding): print("The video cannot be decoded")case let .error(kind): print("Failed:", kind)default: break}Now#
switch try engine.block.getState(block) {case .error(.videoDecoding, _): print("The video cannot be decoded")case let .error(kind, engineError): print("Failed:", kind, engineError?.message ?? "")default: break}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:
let options = VideoExportOptions(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.
A slow resource can also reach this limit, because three steps report no progress or only whole percents while they run: an asynchronous URI resolver, the demux of a video and a video transcode on iOS. A block that you set to pending with setState(_:state:) 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:
try engine.editor.setSettingFloat("exportPendingTimeoutSeconds", value: 0)Text Variables in Text Ranges#
From v1.85, the text range functions, such as getTextColors(_:in:), setTextColor(_:color:in:) and getTextCursorRange(), count a text variable such as {{name}} by its characters, as in text/text. Before v1.85, the engine counted a variable as one character, so a range of text/text after a variable pointed to the wrong characters, 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 moved its ranges to make up for this. Remove that step: a range of text/text now selects the characters it names.
Animations Belong to One Block#
From v1.85, setInAnimation(_:animation:), setLoopAnimation(_:animation:) and setOutAnimation(_:animation:) 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#
let fade = try engine.block.createAnimation(.fade)try engine.block.setInAnimation(first, animation: fade)try engine.block.setInAnimation(second, animation: fade)Now#
try engine.block.setInAnimation(first, animation: engine.block.createAnimation(.fade))try engine.block.setInAnimation(second, animation: engine.block.createAnimation(.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(_:animation:), setOutAnimation(_:animation:) or setLoopAnimation(_:animation:) on it after you add it to the track:
try engine.block.appendChild(to: captionTrack, child: caption)try engine.block.setInAnimation(caption, animation: engine.block.createAnimation(.fade))Percent Sizes with maintainCrop#
From v1.85, setWidth(_:value:maintainCrop:) and setHeight(_:value: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#
try engine.block.setWidth(block, value: 400, maintainCrop: true)Now#
let width = try 400 * engine.block.getWidth(block) / engine.block.getFrameWidth(block)try engine.block.setWidth(block, value: width, maintainCrop: true)Block Audio Exports#
From v1.85, an audio/mp4 export of an audio block or a video block with exportAudio(_:mimeType:options:) 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 AAC, like the export of a page. It fails where the export of a page fails: 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(_:mimeType:options:) of a block, a track or a group also applies the timeOffset of its options 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.