Stop an export, a load or a save that your app no longer needs, or give it a deadline, by cancelling the coroutine that runs it.

Exports, loads, downloads and saves go on in the engine while their suspend function waits. When a user closes a
screen or picks another file, your app no longer needs that work. Each engine call that exports, loads, downloads or
saves stops its work in the engine when you cancel its coroutine. The call then throws CancellationException, and
the engine frees what the call holds.
This guide covers how to cancel a call, which calls you can cancel, how to give a call a deadline, how to cancel from a progress callback, and what the engine keeps after a cancel.
Using the Built-in UI#
The video export dialog of the Video Editor Starter Kit shows the progress of the export and a Close button. The button asks for a confirmation, then cancels the export. This guide covers the programmatic approach, which works the same in the editor and in a headless engine.
Cancelling a Call#
Launch the call in a coroutine scope that you own, such as lifecycleScope or viewModelScope, and keep the Job.
Here we export a page that holds an image block. The image still downloads when the export starts, so the export has
work to stop.
val export = launch { try { engine.block.export(block = page, mimeType = MimeType.PNG) Log.i(TAG, "The export ended before the cancel") } catch (e: CancellationException) { Log.i(TAG, "The export stopped") throw e }}Call cancel() on the job when the user no longer needs the file, for example from a cancel button. The suspended
call throws CancellationException, so the code after the call does not run. cancelAndJoin() also waits until the
coroutine has ended.
export.cancelAndJoin()The catch block logs the cancel and throws the exception again, so the coroutine ends as cancelled. A call that has
already returned ignores the cancel. You can cancel from any thread, because the cancel reaches the engine on its own
thread. A cancelled scope such as viewModelScope cancels every call that it runs.
Other suspend functions of the engine, such as engine.asset.findAssets(), also throw CancellationException when
you cancel their coroutine. Their work in the engine runs to its end, and its result is dropped.
Calls That Can Be Cancelled#
These calls stop their work in the engine when you cancel their coroutine:
- Exports:
engine.block.export()for images and PDF, also into anOutputStream,engine.block.exportWithColorMask(),engine.block.exportVideo(). The PDF export as aFlowof chunks stops when you cancel the coroutine that collects it. - Scene and block loads:
engine.scene.load()from aUri, or from a string withwaitForResources,engine.scene.loadArchive(),engine.scene.applyTemplate()from aUri,engine.block.loadFromURL(),engine.block.loadFromArchive() - New scenes from media:
engine.scene.createFromImage(),engine.scene.createFromVideo() - Downloads for a block or an asset:
engine.block.createCaptionsFromURI(),engine.block.addImageFileUriToSourceSet(),engine.block.addVideoFileUriToSourceSet(),engine.block.forceLoadAVResource(),engine.block.forceLoadResources(),engine.asset.addLocalSourceFromJSON(), andengine.asset.applyAssetSourceAsset()andengine.asset.defaultApplyAsset()without a target block - Lookups:
engine.editor.getMimeType(),engine.editor.getFontMetrics(),engine.block.getDominantColors() - Color profiles:
engine.scene.setCMYKProfile(),engine.editor.loadCMYKProfile() - Saves:
engine.scene.saveToArchive(),engine.block.saveToArchive()
engine.scene.saveToString() and engine.block.saveToString() have no callback that waits for your app, so they end
within the call and have nothing to cancel. Android has no audio export.
Giving a Call a Deadline#
Wrap the call in withTimeout(). The engine has no timeout of its own, so the timeout is the deadline. At the
deadline the call stops, and withTimeout() throws TimeoutCancellationException. Here we give the scene load ten
seconds.
val sceneLoadedInTime = try { withTimeout(10_000) { engine.scene.load(sceneUri = sceneUri) } Log.i(TAG, "The scene loaded in time") true} catch (e: TimeoutCancellationException) { Log.i(TAG, "The scene did not load within 10 seconds") false}Catch TimeoutCancellationException to tell a missed deadline from a failed download. A failed download throws an
EngineException, which this snippet does not catch. withTimeoutOrNull() returns null at the deadline instead of
throwing.
Cancelling From a Progress Callback#
You can cancel the export coroutine inside progressCallback of exportVideo(). Here we stop a video export that has
rendered less than half of its frames after one minute.
suspend fun exportVideoUnlessSlow( engine: Engine, page: DesignBlock,): ByteBuffer? = coroutineScope { val startedAt = SystemClock.elapsedRealtime() val export = async { engine.block.exportVideo( block = page, timeOffset = 0.0, duration = engine.block.getDuration(page), mimeType = MimeType.MP4, progressCallback = { progress -> val elapsedMillis = SystemClock.elapsedRealtime() - startedAt // Less than half of the frames after one minute: the export takes too long. if (elapsedMillis > 60_000 && progress.renderedFrames < progress.totalFrames / 2) { this@async.cancel() } }, ) } try { export.await().also { video -> Log.i(TAG, "The video file has ${video.remaining()} bytes") } } catch (e: CancellationException) { // Throws again when the caller was cancelled, not the slow export. ensureActive() Log.i(TAG, "The export was too slow and stopped") null }}this@async.cancel() cancels only the export coroutine, not the caller. In the catch block, ensureActive() throws
again when the caller itself was cancelled. Once exportVideo() has thrown, the callback gets no more reports, and the
next export can start at once. The time of the first reports includes the start-up of the export, so an estimate of
the total time from them would also stop fast exports.
What a Cancel Stops and What It Keeps#
A cancel stops the work of the call, but the engine keeps what other blocks and calls still need:
- An export runs in a background engine. A cancel ends the export and stops that engine, which frees what the export holds. The next export starts a new background engine.
- A load stops its download, unless a block or another call needs the same file. The engine reads every video fill at each update, so the download of a video fill goes on.
engine.scene.load()from aUri,loadArchive(),applyTemplate(),createFromImage()andcreateFromVideo()keep the current scene. WithwaitForResources,load()from aUrireplaces the scene before the wait for its resources, so a cancel during that wait keeps the new scene.load()from a string withwaitForResourcesloads the scene at once, so a cancel ends only the wait.loadCMYKProfile()ends only the wait, because the document keeps its profile loaded.- A cancelled
setCMYKProfile()gives back its claim, so an assignment that it replaced still takes effect. applyAssetSourceAsset()stops the download that the default apply waits for, so it creates no block. An asset source with its ownapplyAssetkeeps running in your code. A block that it creates stays, and the call does not return it.getDominantColors()waits while a resource of the block loads, sowithTimeout()gives it a deadline.
Starting the Next Call#
After the call throws, the engine is free for the next export or load. Start it after cancelAndJoin() returns, or in
a new coroutine. Here we export the same page again, this time without a cancel.
val png = engine.block.export(block = page, mimeType = MimeType.PNG)Log.i(TAG, "The next export made a PNG file of ${png.remaining()} bytes")The second export waits for the image that the first export waited for, because the image block still needs it.
Troubleshooting#
- The code after a cancelled call runs: A
catch (e: Exception)around the call also catchesCancellationException. Throw it again, or catch only the exceptions that you expect. - The call returns its result although you cancelled: The call ended before the cancel.
- The coroutine ends without an error at the deadline: An uncaught
TimeoutCancellationExceptionends the coroutine as cancelled. Catch it aroundwithTimeout(), or usewithTimeoutOrNull(). - The download of a cancelled load goes on: A block on the canvas or another call needs the same file.
API Reference#
These are the methods this guide calls. The section Calls That Can Be Cancelled lists every method that stops its work in the engine on a cancel.
| Method | Category | Purpose |
|---|---|---|
engine.block.export(block=_, mimeType=_) |
Block | Export a block as an image or a PDF |
engine.block.exportVideo(block=_, timeOffset=_, duration=_, mimeType=_, progressCallback=_) |
Block | Export a page as a video |
engine.block.getDuration(block=_) |
Block | Get the playback duration of a block |
engine.scene.load(sceneUri=_) |
Scene | Load a scene or an archive from a Uri |
Next Steps#
- Export - Export designs and videos
- Resources - Learn how the engine loads files
- Scenes - Load and save scenes
- Error Catalog - Look up engine error codes