<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Team Banuba</title>
    <description>The latest articles on DEV Community by Team Banuba (@team_banuba).</description>
    <link>https://dev.to/team_banuba</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4082753%2Fc1c62964-e75a-4e5c-a2b1-4d1001031685.png</url>
      <title>DEV Community: Team Banuba</title>
      <link>https://dev.to/team_banuba</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/team_banuba"/>
    <language>en</language>
    <item>
      <title>Flutter Video Editor in 6 Steps: Banuba SDK Setup, Entry Screens and Export</title>
      <dc:creator>Team Banuba</dc:creator>
      <pubDate>Tue, 03 Nov 2026 13:35:51 +0000</pubDate>
      <link>https://dev.to/team_banuba/flutter-video-editor-in-6-steps-banuba-sdk-setup-entry-screens-and-export-1h6o</link>
      <guid>https://dev.to/team_banuba/flutter-video-editor-in-6-steps-banuba-sdk-setup-entry-screens-and-export-1h6o</guid>
      <description>&lt;p&gt;A Flutter video editor built on Banuba's ve_sdk_flutter plugin is one Dart call that opens a native recording and editing screen and returns the exported file paths to your code. The setup is six steps: pin the package, configure iOS, configure Android, launch the editor, pick an entry screen, and set the export.&lt;/p&gt;

&lt;p&gt;Banuba Video Editor SDK is a production-ready mobile video editing engine for iOS, Android, Flutter and React Native. We build it, so this guide uses our plugin, and the comparison near the end also covers the open-source Flutter packages we do not sell.&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before you add a video editor to Flutter?
&lt;/h2&gt;

&lt;p&gt;You need four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A license token.&lt;/strong&gt; Banuba issues it with a 14-day free trial of the &lt;a href="https://www.banuba.com/video-editor-sdk-best-for-developers" rel="noopener noreferrer"&gt;Video Editor SDK&lt;/a&gt;, and the plugin's pub.dev page says no credit card information is needed. Without a token, every call fails with an error code you will meet in Step 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Flutter toolchain at or above the plugin's floor.&lt;/strong&gt; The &lt;a href="https://pub.dev/packages/ve_sdk_flutter" rel="noopener noreferrer"&gt;pub.dev page&lt;/a&gt; lists Dart 3.3.0, Flutter 3.19.2, Android 8.0+ and iOS 15.0+.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native tools for both platforms.&lt;/strong&gt; Our requirements pages list Kotlin 2.1+ or Java 17, the Camera 2 API and OpenGL ES 3.0 on Android, and Swift 5.9, Xcode 26.0 and iOS 15.0 on Apple devices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Physical devices.&lt;/strong&gt; Our Android requirements mark x86-64 as limited support and x86 as not supported, so an emulator image is a weak place to judge the editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: How do you install the Flutter video editor plugin?
&lt;/h2&gt;

&lt;p&gt;Add the package with the version pinned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ve_sdk_flutter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0.48.1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;flutter pub get&lt;/code&gt;. The ve_sdk_flutter package depends only on &lt;code&gt;flutter&lt;/code&gt; and &lt;code&gt;plugin_platform_interface&lt;/code&gt;, so it does not pull a media library into your Dart dependency tree. The media engine arrives through CocoaPods and Gradle in the next two steps.&lt;/p&gt;

&lt;p&gt;Pin the exact version on purpose, and read the changelog rather than the version number. Some releases track native SDK versions: 0.43.0 supports Video Editor 1.52.0 and 0.44.0 supports 1.53.0, so a minor bump can change native behavior under the same Dart code. Others touch nothing functional at all, which is what 0.48.0 and 0.48.1 did, updating the package description, repository link, topics and homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: What does iOS need?
&lt;/h2&gt;

&lt;p&gt;Two edits. First, add Banuba's spec repositories at the top of &lt;code&gt;ios/Podfile&lt;/code&gt;, as the &lt;a href="https://github.com/Banuba/ve-sdk-flutter/blob/master/mddocs/integration_guide.md" rel="noopener noreferrer"&gt;plugin integration guide&lt;/a&gt; shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="ss"&gt;:ios&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'15.0'&lt;/span&gt;

&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/CocoaPods/Specs.git'&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/Banuba/specs.git'&lt;/span&gt;
&lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="s1"&gt;'https://github.com/sdk-banuba/banuba-sdk-podspecs.git'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Second, add the four usage descriptions to &lt;code&gt;ios/Runner/Info.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSAppleMusicUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app requires access to the media library&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSCameraUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app requires access to the camera.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSMicrophoneUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app requires access to the microphone.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;NSPhotoLibraryUsageDescription&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;This app requires access to the photo library.&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;pod install&lt;/code&gt; inside &lt;code&gt;ios&lt;/code&gt;, then select your Development Team under Signing &amp;amp; Capabilities. Replace the sample strings with wording that tells your users why you need each permission.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: What does Android need, and how do you keep the APK small?
&lt;/h2&gt;

&lt;p&gt;On Android, the size decision is the main one. Our documentation gives these additions to your app's download size, and we did not re-measure them for this article:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you ship&lt;/th&gt;
&lt;th&gt;Android&lt;/th&gt;
&lt;th&gt;iOS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Core editor&lt;/td&gt;
&lt;td&gt;25 MB&lt;/td&gt;
&lt;td&gt;14 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full bundle&lt;/td&gt;
&lt;td&gt;45 MB&lt;/td&gt;
&lt;td&gt;45 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor with face AR features&lt;/td&gt;
&lt;td&gt;45 to 90 MB&lt;/td&gt;
&lt;td&gt;45 to 140 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two switches move you between those rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn off face AR if you do not need masks.&lt;/strong&gt; Add &lt;code&gt;ENABLE_FACE_AR=false&lt;/code&gt; to &lt;code&gt;android/gradle.properties&lt;/code&gt;, and this line to the Podfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ENABLE_FACE_AR'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'false'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Filter processor architectures.&lt;/strong&gt; The integration guide notes that each architecture adds megabytes. Ship ARM only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;ndk&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;abiFilters&lt;/span&gt; &lt;span class="s1"&gt;'armeabi-v7a'&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'arm64-v8a'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upload an Android App Bundle as well. Our Android docs state that with a bundle, the per-device library comes out 24 to 26 MB smaller than the documented figure, because Google Play serves each device only its own architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: How do you open the video editor from Dart?
&lt;/h2&gt;

&lt;p&gt;This is a complete &lt;code&gt;lib/main.dart&lt;/code&gt;. One button opens the camera screen, and the result or the error comes back to the same widget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/material.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/services.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:ve_sdk_flutter/errors.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:ve_sdk_flutter/export_data.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:ve_sdk_flutter/export_result.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:ve_sdk_flutter/features_config.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:ve_sdk_flutter/ve_sdk_flutter.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Passed at build time: flutter run --dart-define=BANUBA_TOKEN=your_token&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_licenseToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'BANUBA_TOKEN'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;runApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MaterialApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;home:&lt;/span&gt; &lt;span class="n"&gt;EditorLauncher&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EditorLauncher&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;StatefulWidget&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;EditorLauncher&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EditorLauncher&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;createState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_EditorLauncherState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_EditorLauncherState&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EditorLauncher&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;_veSdk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VeSdkFlutter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'No clip yet'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 6: a 720p file in H.264 plus a file at device-chosen quality&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;_exportData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ExportData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;exportedVideos:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;ExportedVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;fileName:&lt;/span&gt; &lt;span class="s"&gt;'clip_720p'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;videoResolution:&lt;/span&gt; &lt;span class="n"&gt;VideoResolution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hd720p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;useHevcIfPossible:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;ExportedVideo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;fileName:&lt;/span&gt; &lt;span class="s"&gt;'clip_auto'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;videoResolution:&lt;/span&gt; &lt;span class="n"&gt;VideoResolution&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_openEditor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FeaturesConfigBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ExportResult&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_veSdk&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openCameraScreen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_licenseToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nl"&gt;exportData:&lt;/span&gt; &lt;span class="n"&gt;_exportData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s"&gt;'Editor closed before export'&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'Exported: &lt;/span&gt;&lt;span class="si"&gt;${result.videoSources.join(', ')}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;on&lt;/span&gt; &lt;span class="n"&gt;PlatformException&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;_describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PlatformException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;errCodeSdkNotInitialized:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'License token is empty or invalid'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;errCodeSdkLicenseRevoked:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'License revoked or expired'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;errCodeMissingHost:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'No Activity or ViewController to show the editor'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;errCodeInvalidParams:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'The editor received invalid input'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;errCodeMissingExportResult:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'The editor could not export the video'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;'Error &lt;/span&gt;&lt;span class="si"&gt;${e.code}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;${e.message}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nd"&gt;@override&lt;/span&gt;
  &lt;span class="n"&gt;Widget&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BuildContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Scaffold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nl"&gt;body:&lt;/span&gt; &lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="nl"&gt;mainAxisSize:&lt;/span&gt; &lt;span class="n"&gt;MainAxisSize&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nl"&gt;children:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_status&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;SizedBox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;height:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;ElevatedButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
              &lt;span class="nl"&gt;onPressed:&lt;/span&gt; &lt;span class="n"&gt;_openEditor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="nl"&gt;child:&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Open video editor'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;openCameraScreen&lt;/code&gt; returns &lt;code&gt;Future&amp;lt;ExportResult?&amp;gt;&lt;/code&gt;. A null result means the user closed the editor without exporting, so a user who backs out halfway still leaves your screen with a defined status.&lt;/p&gt;

&lt;p&gt;The plugin defines five error codes in &lt;code&gt;errors.dart&lt;/code&gt;. The three below are the ones that stop a first run, and a fourth failure mode after them throws nothing at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  ERR_SDK_NOT_INITIALIZED
&lt;/h3&gt;

&lt;p&gt;The plugin's comment on this code reads: "Error occurs when the sdk cannot be initialized with provided license token: invalid, empty". With the snippet above, it usually means the app was started without &lt;code&gt;--dart-define=BANUBA_TOKEN=...&lt;/code&gt;, so the constant is an empty string.&lt;/p&gt;

&lt;h3&gt;
  
  
  ERR_SDK_LICENSE_REVOKED
&lt;/h3&gt;

&lt;p&gt;The license has been revoked, and the plugin's own comment says to contact Banuba. In the integration sample, the same code also covers an expired license, which is what a finished 14-day trial looks like from Dart.&lt;/p&gt;

&lt;h3&gt;
  
  
  ERR_MISSING_HOST
&lt;/h3&gt;

&lt;p&gt;The editor is a native screen, so it needs an Activity on Android or a ViewController on iOS to present from. This code means none was available, which points at calling the plugin before the first frame has rendered, for example from &lt;code&gt;main()&lt;/code&gt; or &lt;code&gt;initState()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  "New UI is not available from Trimmer screen"
&lt;/h3&gt;

&lt;p&gt;This one is not an error code, and that is what makes it expensive to debug. &lt;code&gt;FeaturesConfigBuilder&lt;/code&gt; sets &lt;code&gt;enableEditorV2&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; by default, and with the new UI enabled the plugin refuses the trimmer entry point: it writes that line to the debug console and returns &lt;code&gt;null&lt;/code&gt; without throwing.&lt;/p&gt;

&lt;p&gt;From the snippet above, the only thing you see is "Editor closed before export", as though the user had backed out. If you want &lt;code&gt;openTrimmerScreen&lt;/code&gt;, turn the new UI off for that call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FeaturesConfigBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enableEditorV2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Which entry screen should the editor open with?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;VeSdkFlutter&lt;/code&gt; in 0.48.1 exposes eight entry points. Each takes the token and a &lt;code&gt;FeaturesConfig&lt;/code&gt;, and each returns the same &lt;code&gt;ExportResult&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Extra input&lt;/th&gt;
&lt;th&gt;Starts the user on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openCameraScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Recording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openPipScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one video path&lt;/td&gt;
&lt;td&gt;Picture-in-picture over an existing clip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openTrimmerScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list of video paths&lt;/td&gt;
&lt;td&gt;Trimming the clips you pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openEditorScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list of video paths&lt;/td&gt;
&lt;td&gt;The editing timeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openAiClippingScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;AI clipping, starting from the gallery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openTemplatesScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Video templates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openDraftsScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;Saved drafts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openGalleryScreen&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;The media gallery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;openPipScreen&lt;/code&gt; takes one video path on top of the token and config, and &lt;code&gt;openTrimmerScreen&lt;/code&gt; and &lt;code&gt;openEditorScreen&lt;/code&gt; take a list of paths, which you get from a picker such as &lt;code&gt;image_picker&lt;/code&gt;. The other five need nothing else.&lt;/p&gt;

&lt;p&gt;Two things in the 0.48.1 source are worth knowing before you build on this table. &lt;code&gt;openTrimmerScreen&lt;/code&gt; is the one that returns null under the default config, as described above. And &lt;code&gt;openEditorScreen&lt;/code&gt; accepts an optional &lt;code&gt;trackData&lt;/code&gt; argument that goes nowhere: the plugin serializes track data and sends it to the native side from its internal open method, but the editor entry point does not forward the argument to it. Passing a track there is a silent no-op today, so preload audio another way until a release says otherwise.&lt;/p&gt;

&lt;p&gt;Each screen is trimmed through &lt;code&gt;FeaturesConfigBuilder&lt;/code&gt;. Setting &lt;code&gt;supportsBeauty&lt;/code&gt; and &lt;code&gt;supportsMasks&lt;/code&gt; to false in &lt;code&gt;CameraConfig&lt;/code&gt; removes the beauty and mask controls from the camera, and &lt;code&gt;supportsVoiceOver: false&lt;/code&gt; in &lt;code&gt;EditorConfig&lt;/code&gt; removes voice-over, which our docs mark as Android only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: How do you control the exported file?
&lt;/h2&gt;

&lt;p&gt;Without &lt;code&gt;ExportData&lt;/code&gt;, the plugin exports one video at auto quality, which our docs describe as chosen from the device's hardware. With it, you name each file and pick a &lt;code&gt;VideoResolution&lt;/code&gt;. The enum has nine values: seven resolutions from &lt;code&gt;vga360p&lt;/code&gt; through &lt;code&gt;vga480p&lt;/code&gt;, &lt;code&gt;qhd540p&lt;/code&gt;, &lt;code&gt;hd720p&lt;/code&gt;, &lt;code&gt;fhd1080p&lt;/code&gt; and &lt;code&gt;qhd1440p&lt;/code&gt; to &lt;code&gt;uhd2160p&lt;/code&gt;, plus &lt;code&gt;auto&lt;/code&gt; and &lt;code&gt;original&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The file type differs by platform. Our docs state that the video is exported as .mp4 on Android and .mov on iOS, so do not hard-code an extension when you upload.&lt;/p&gt;

&lt;p&gt;Three settings change what lands on disk:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;useHevcIfPossible: false&lt;/code&gt; switches the file to H.264 (AVC), for upload or playback pipelines that expect AVC.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Watermark(imagePath: 'assets/watermark.png', alignment: WatermarkAlignment.bottomRight)&lt;/code&gt; adds your logo. Our docs state that no watermark is added by default.&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;ExportedVideo&lt;/code&gt; entry produces its own file, so two entries mean two encodes on the user's phone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result carries more than video paths. &lt;code&gt;ExportResult&lt;/code&gt; also returns &lt;code&gt;previewFilePath&lt;/code&gt;, &lt;code&gt;metaFilePath&lt;/code&gt; and &lt;code&gt;audioMeta&lt;/code&gt;, and the plugin's example describes the meta file as short data used in the exported video. To play the exported file inside your own screen, the official &lt;code&gt;video_player&lt;/code&gt; plugin is enough.&lt;/p&gt;

&lt;p&gt;Encoding time depends on the phone, so measure the export on the slowest Android model you support. Our &lt;a href="https://www.banuba.com/blog/ve-sdk-performance-benchmark-compared" rel="noopener noreferrer"&gt;video editor SDK performance benchmark&lt;/a&gt; shows how results spread across devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which Flutter video editor route fits your app?
&lt;/h2&gt;

&lt;p&gt;Video editing in Flutter has four realistic routes, and each is the right call for some team.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Banuba plugin (ve_sdk_flutter 0.48.1)&lt;/th&gt;
&lt;th&gt;Banuba integration sample&lt;/th&gt;
&lt;th&gt;video_editor 3.0.0&lt;/th&gt;
&lt;th&gt;ffmpeg_kit_flutter&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;Ready editor UI behind one Dart call&lt;/td&gt;
&lt;td&gt;Full app source using a platform channel&lt;/td&gt;
&lt;td&gt;Dart widgets for trim, crop, rotate and cover&lt;/td&gt;
&lt;td&gt;FFmpeg and FFprobe command runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editing UI&lt;/td&gt;
&lt;td&gt;Included, native screens&lt;/td&gt;
&lt;td&gt;Included, editable native code&lt;/td&gt;
&lt;td&gt;You assemble it from widgets&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Export&lt;/td&gt;
&lt;td&gt;Included, mp4 or mov&lt;/td&gt;
&lt;td&gt;Included&lt;/td&gt;
&lt;td&gt;Builds FFmpeg commands, does not export&lt;/td&gt;
&lt;td&gt;Any FFmpeg command you write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Access&lt;/td&gt;
&lt;td&gt;Trial token on request&lt;/td&gt;
&lt;td&gt;Trial token on request&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;td&gt;Open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;Commercial&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;td&gt;LGPL 3.0 by default, some packages GPL v3.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance state&lt;/td&gt;
&lt;td&gt;Released against current native SDK versions&lt;/td&gt;
&lt;td&gt;Tracks the plugin&lt;/td&gt;
&lt;td&gt;Latest release is 3.0.0, no release since&lt;/td&gt;
&lt;td&gt;Upstream FFmpegKit retired; successor and community forks exist&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four ways to read that table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fastest to a complete editor: the Banuba plugin.&lt;/strong&gt; Recording, timeline, effects and export arrive behind the call in Step 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most control over native behavior: the Banuba integration sample.&lt;/strong&gt; The &lt;a href="https://github.com/Banuba/ve-sdk-flutter-integration-sample" rel="noopener noreferrer"&gt;Flutter integration sample on GitHub&lt;/a&gt; ships the app source, so your team can change the native layer rather than wait for a plugin release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost and UI ownership: video_editor.&lt;/strong&gt; It is free under MIT and every widget is yours to style. You still need an export path, because its README says the library does not handle exportation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raw codec control: the FFmpeg route.&lt;/strong&gt; No other option lets you write arbitrary FFmpeg commands, and the licensing is the thing to settle first, since the default build is LGPL 3.0 and some variants are GPL v3.0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The FFmpeg route needs one more sentence, because the package most guides still point at is not where the work happens now. The upstream project posted a retirement notice in July 2026 stating that FFmpegKit has been officially retired, and names &lt;a href="https://github.com/arthenica/ffmpeg-kit" rel="noopener noreferrer"&gt;FFmpegKitNext&lt;/a&gt; as the actively maintained continuation, distributed as source only, alongside community-maintained forks published for Android, Flutter and React Native through the usual package managers. A team going this way is picking a fork and a build process, not just adding a dependency.&lt;/p&gt;

&lt;p&gt;One more limit of our own: the Banuba editor does not run on the web, so a Flutter web target needs a different route for that platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use Banuba for a Flutter video editor?
&lt;/h2&gt;

&lt;p&gt;Banuba Video Editor SDK covers recording, a multi-track timeline, effects, AI clipping, AI subtitles and export in one plugin. Our docs state that it works offline and does not send, store or process user information. They also publish agent skills for Claude Code, Codex and Qwen Code, plus an LLM-friendly documentation file for AI-assisted coding.&lt;/p&gt;

&lt;p&gt;It does not hand you Dart widgets to restyle line by line. The screens are native, and you shape them through &lt;code&gt;FeaturesConfig&lt;/code&gt;, so a team that wants a pixel-level custom UI in Flutter is better served by video_editor plus its own export path.&lt;/p&gt;

&lt;p&gt;For the native side of the same editor, see our walkthrough of &lt;a href="https://banuba.medium.com/ios-video-editor-sdk-swift-b444565f1de1" rel="noopener noreferrer"&gt;integrating the iOS video editor SDK in Swift&lt;/a&gt;. For other vendors, our &lt;a href="https://banuba.medium.com/video-editor-sdk-social-apps-7cbf439b7136" rel="noopener noreferrer"&gt;comparison of five video editor SDKs for social apps&lt;/a&gt; puts Banuba next to the alternatives, and the &lt;a href="https://www.banuba.com/blog/build-video-editor-in-flutter" rel="noopener noreferrer"&gt;original Flutter guide on our blog&lt;/a&gt; covers the feature list in more depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six steps take a Flutter app from pubspec to an exported clip
&lt;/h2&gt;

&lt;p&gt;Pin ve_sdk_flutter at 0.48.1, add the Podfile sources and permissions, set the Android size switches, and call one &lt;code&gt;open...Screen&lt;/code&gt; method from a mounted widget. Then decide the export: which resolutions, which codec, and whether a watermark goes on.&lt;/p&gt;

&lt;p&gt;Check the size table in Step 3 and the four failure modes in Step 4 before the first device run. Three of them throw, and the fourth returns null and looks like a user walking away.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is the Banuba Flutter video editor free?
&lt;/h3&gt;

&lt;p&gt;The Banuba Flutter video editor is free for a 14-day trial with the full feature set, and the plugin's pub.dev page says no credit card information is needed. Production use needs a commercial Banuba license.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Banuba Flutter video editor work offline?
&lt;/h3&gt;

&lt;p&gt;Yes. Banuba's documentation states that the Video Editor SDK works offline and does not send, store or process user information.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the Banuba Video Editor SDK support Flutter web?
&lt;/h3&gt;

&lt;p&gt;No. The Banuba Video Editor SDK runs on iOS and Android, and pub.dev lists only those two platforms for ve_sdk_flutter.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I fix ERR_SDK_NOT_INITIALIZED in the Banuba Flutter plugin?
&lt;/h3&gt;

&lt;p&gt;The Banuba plugin returns ERR_SDK_NOT_INITIALIZED when the license token is empty or invalid. Check that the token reaches the call, for example that you passed &lt;code&gt;--dart-define=BANUBA_TOKEN=...&lt;/code&gt; when you started the app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does openTrimmerScreen return null in the Banuba Flutter plugin?
&lt;/h3&gt;

&lt;p&gt;Because the new editor UI is on by default. &lt;code&gt;FeaturesConfigBuilder&lt;/code&gt; sets &lt;code&gt;enableEditorV2&lt;/code&gt; to true, and the plugin blocks the trimmer entry point under it, logging "New UI is not available from Trimmer screen" and returning null rather than throwing. Call &lt;code&gt;.enableEditorV2(false)&lt;/code&gt; on the builder if you need that screen.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does the Banuba video editor add to a Flutter app's size?
&lt;/h3&gt;

&lt;p&gt;Banuba documents 25 MB on Android and 14 MB on iOS for the core editor, 45 MB for the full bundle, and 45 to 90 MB on Android or 45 to 140 MB on iOS with face AR. Turning face AR off and filtering architectures moves a build toward the core figures.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>android</category>
      <category>ios</category>
    </item>
    <item>
      <title>Face AR SDK Integration on Android and iOS: 6 Steps From Token to First Effect</title>
      <dc:creator>Team Banuba</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:37:46 +0000</pubDate>
      <link>https://dev.to/team_banuba/face-ar-sdk-integration-on-android-and-ios-6-steps-from-token-to-first-effect-5lk</link>
      <guid>https://dev.to/team_banuba/face-ar-sdk-integration-on-android-and-ios-6-steps-from-token-to-first-effect-5lk</guid>
      <description>&lt;p&gt;Adding face AR to an existing mobile app is a six-step integration that touches two code files per platform, plus your build files. Banuba Face AR SDK is a real-time, on-device face tracking and AR effects SDK. Per Banuba's own technical specification it tracks a single face at 30 FPS on the Android High, iOS Mid and iOS High device tiers and at 25 FPS on Android Low, within a head-angle range of 80 degrees. The full table is in step 6.&lt;/p&gt;

&lt;p&gt;The six steps are: get a client token, add the SDK packages, initialize once at app start, connect the camera to a player and an output view, load an effect, and handle the lifecycle. This guide is for a mobile developer who already ships an Android or iOS app and wants a face filter running in the camera view. The code below is taken from the official Banuba sample repositories for &lt;a href="https://github.com/Banuba/banuba-sdk-android-samples" rel="noopener noreferrer"&gt;Android&lt;/a&gt; and &lt;a href="https://github.com/Banuba/banuba-sdk-ios-samples" rel="noopener noreferrer"&gt;iOS&lt;/a&gt;, so every call is one you can open and run. We build the SDK, and every figure named below comes from our own documentation; this guide contains no benchmark of our own.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Of7-xNDYknY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What do you need before you add a face AR SDK to an app?
&lt;/h2&gt;

&lt;p&gt;Four things, and all of them are checkable before you write a line.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A device and OS floor. Banuba Face AR SDK supports Android from 8.0 (API level 26) and iOS from 13.0, and needs OpenGL ES 3.0 or higher on any mobile platform, per the &lt;a href="https://docs.banuba.com/far-sdk/tutorials/capabilities/system_requirements" rel="noopener noreferrer"&gt;system requirements&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;A camera. The iOS sample opens the front camera at the 1280x720 preset.&lt;/li&gt;
&lt;li&gt;A language. The SDK exposes Kotlin and Java on Android, Swift and Objective-C on iOS.&lt;/li&gt;
&lt;li&gt;A client token, which is step 1.
One decision belongs here rather than later. If your app also targets Flutter or React Native, the same SDK ships as Banuba-maintained plugins, and our earlier walkthrough on &lt;a href="https://medium.com/@banuba/integrating-augmented-reality-features-to-react-native-apps-afb2eae71962" rel="noopener noreferrer"&gt;adding AR features to a React Native app&lt;/a&gt; covers that path. This guide stays native.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Get a client token and decide where it lives
&lt;/h2&gt;

&lt;p&gt;Banuba Face AR SDK does nothing without a token. There are two kinds, per the &lt;a href="https://docs.banuba.com/far-sdk/tutorials/capabilities/token_management" rel="noopener noreferrer"&gt;token management&lt;/a&gt; page: a demo token for the trial, valid for 14 days and activating all SDK features, and a commercial token issued after payment, valid for the prepaid period and activating the features in your license. You request the demo token through the form on the &lt;a href="https://www.banuba.com/facear-sdk/face-filters" rel="noopener noreferrer"&gt;Face AR SDK page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The decision at this step is storage. Banuba recommends storing the token on your server, because it makes renewal faster. If the token is compiled into the app, every renewal means a new build submitted to the App Store or Google Play.&lt;/p&gt;

&lt;p&gt;Where this step fails: an expired token. Banuba documents three states. While the token is valid, the SDK works as expected. For one month after expiry, the SDK works but shows a Banuba watermark. After that month, the functionality stops while the rest of your app keeps running. A demo token in a live app hits the first of those states 14 days after you got it, which is why the docs say not to ship one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Add the SDK packages, and only the ones you need
&lt;/h2&gt;

&lt;p&gt;Banuba Face AR SDK is split into packages, and your app pulls the ones its features need. Three rules from the &lt;a href="https://docs.banuba.com/far-sdk/tutorials/development/installation" rel="noopener noreferrer"&gt;installation page&lt;/a&gt; decide the step.&lt;/p&gt;

&lt;p&gt;First, the API package is always required. On Android that is &lt;code&gt;com.banuba.sdk:sdk_api&lt;/code&gt;, on iOS it is &lt;code&gt;BNBSdkApi&lt;/code&gt;. Second, face tracking lives in its own package, &lt;code&gt;face_tracker&lt;/code&gt; on Android and &lt;code&gt;BNBFaceTracker&lt;/code&gt; on iOS, and the docs say to include it whenever you deal with tracking. Third, every package must be the same version. Banuba warns that packages of different versions, even minor ones, may conflict or work incorrectly together.&lt;/p&gt;

&lt;p&gt;On Android, add the Banuba Maven repository in &lt;code&gt;settings.gradle.kts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencyResolutionManagement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;repositories&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;google&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;mavenCentral&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;maven&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"BanubaMaven"&lt;/span&gt;
            &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://nexus.banuba.net/repository/maven-releases"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare the packages. Note the underscores in the artifact names and the exact version string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;bnbVersion&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1.18.5"&lt;/span&gt;

&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Always required&lt;/span&gt;
    &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.banuba.sdk:sdk_api:$bnbVersion"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// Face tracking is used in most effects&lt;/span&gt;
    &lt;span class="nf"&gt;api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"com.banuba.sdk:face_tracker:$bnbVersion"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Add only what your effects actually use:&lt;/span&gt;
    &lt;span class="c1"&gt;// api("com.banuba.sdk:background:$bnbVersion")   // background replacement&lt;/span&gt;
    &lt;span class="c1"&gt;// api("com.banuba.sdk:makeup:$bnbVersion")       // makeup&lt;/span&gt;
    &lt;span class="c1"&gt;// api("com.banuba.sdk:eyes:$bnbVersion")         // eye effects&lt;/span&gt;
    &lt;span class="c1"&gt;// api("com.banuba.sdk:lips:$bnbVersion")         // lipstick&lt;/span&gt;
    &lt;span class="c1"&gt;// api("com.banuba.sdk:hair:$bnbVersion")         // hair color&lt;/span&gt;
    &lt;span class="c1"&gt;// api("com.banuba.sdk:skin:$bnbVersion")         // skin retouch&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin the version rather than writing &lt;code&gt;1.18.+&lt;/code&gt;. A dynamic version resolves at build time, so two machines, or the same machine a week apart, can end up on different patch releases. That is the exact situation the docs warn about when they say every package must be on the same version.&lt;/p&gt;

&lt;p&gt;For a face filter alone, &lt;code&gt;sdk_api&lt;/code&gt; plus &lt;code&gt;face_tracker&lt;/code&gt; is the whole list. The &lt;code&gt;banuba_sdk&lt;/code&gt; package depends on everything, and the docs describe it as the choice when you do not care about size.&lt;/p&gt;

&lt;p&gt;On iOS, add the Swift Package Manager packages through &lt;strong&gt;File &amp;gt; Add Package Dependencies&lt;/strong&gt;, starting with &lt;code&gt;https://github.com/sdk-banuba/BNBSdkApi&lt;/code&gt;, and add the feature packages from the &lt;code&gt;sdk-banuba&lt;/code&gt; repositories at the same version. CocoaPods is also supported through a custom repository listed in the &lt;a href="https://github.com/sdk-banuba/banuba-sdk-podspecs" rel="noopener noreferrer"&gt;podspecs repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Where this step fails: a feature that renders wrong or not at all. The installation page says to read the application logs in that case to find which package is missing. The second failure is a version mismatch between two packages, which the same page says can break features without an obvious error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Initialize the SDK once at app start
&lt;/h2&gt;

&lt;p&gt;Initialization is a single call with the token, placed in the application class so it runs once before any camera screen opens.&lt;/p&gt;

&lt;p&gt;Android, from the sample's &lt;code&gt;Application.kt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.banuba.sdk.manager.BanubaSdkManager&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;BANUBA_CLIENT_TOKEN&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nc"&gt;Place&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;android&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nc"&gt;BanubaSdkManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BANUBA_CLIENT_TOKEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS, from the sample's &lt;code&gt;AppDelegate.swift&lt;/code&gt;. The &lt;code&gt;resourcePath&lt;/code&gt; array tells the SDK where to look for effects, and the sample points it at an &lt;code&gt;effects&lt;/code&gt; folder inside the app bundle and at the bundle root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;UIKit&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;BNBSdkApi&lt;/span&gt;

&lt;span class="kd"&gt;@UIApplicationMain&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;AppDelegate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIResponder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;UIApplicationDelegate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;banubaClientToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;#Place&lt;/span&gt; &lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="n"&gt;here&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIWindow&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="nv"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;didFinishLaunchingWithOptions&lt;/span&gt; &lt;span class="nv"&gt;launchOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;UIApplication&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;LaunchOptionsKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]?&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;BanubaSdkManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;resourcePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bundlePath&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/effects"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="kt"&gt;Bundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bundlePath&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="nv"&gt;clientTokenString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;banubaClientToken&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;#Place your token here#&amp;gt;&lt;/code&gt; placeholder is literally what the samples ship, and neither file compiles until you replace it. Put in the value from step 1, or the call that fetches it from your server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Connect the camera, the player and the output view
&lt;/h2&gt;

&lt;p&gt;This is the step that turns the SDK on. Three objects do the work on both platforms: a camera device that produces frames, a &lt;code&gt;Player&lt;/code&gt; that runs the pipeline, and an output that displays the result. The pattern is &lt;code&gt;player.use(input, output)&lt;/code&gt;, then &lt;code&gt;play()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Android, from the sample's &lt;code&gt;MainActivity.kt&lt;/code&gt;, trimmed to the pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.os.Bundle&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;android.view.SurfaceView&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.banuba.sdk.input.CameraDevice&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.banuba.sdk.input.CameraInput&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.banuba.sdk.output.SurfaceOutput&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.banuba.sdk.player.Player&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;BaseActivity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;surfaceView&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LazyThreadSafetyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;findViewById&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SurfaceView&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;surfaceView&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The player executes the main pipeline&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;player&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LazyThreadSafetyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// This camera device will pass frames to the CameraInput&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;cameraDevice&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LazyThreadSafetyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;CameraDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;requireNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applicationContext&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="nd"&gt;@MainActivity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The result will be displayed on the surface&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;surfaceOutput&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;LazyThreadSafetyMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NONE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SurfaceOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surfaceView&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;holder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;// Set where the player takes input frames from and where it displays the result&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CameraInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cameraDevice&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;surfaceOutput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS, from the sample's &lt;code&gt;ViewController.swift&lt;/code&gt;. The output here is an &lt;code&gt;EffectPlayerView&lt;/code&gt; placed in the storyboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;UIKit&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;BNBSdkApi&lt;/span&gt;
&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;BNBSdkCore&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;ViewController&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;UIViewController&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Output surface for the Player&lt;/span&gt;
    &lt;span class="kd"&gt;@IBOutlet&lt;/span&gt; &lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;effectView&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;EffectPlayerView&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

    &lt;span class="c1"&gt;// Input stream for the Player&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;cameraDevice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CameraDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;cameraMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;FrontCameraSession&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;captureSessionPreset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hd1280x720&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Player processes frames from the input and renders them into the outputs&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;player&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="c1"&gt;// Current effect&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;BNBEffect&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;viewDidLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Camera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cameraDevice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cameraDevice&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;outputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;effectView&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"TrollGrandma"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cameraDevice&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where this step fails: a black view. On Android, nothing is drawn until &lt;code&gt;cameraDevice.start()&lt;/code&gt; and &lt;code&gt;player.play()&lt;/code&gt; have both run, and in the sample those calls sit in the lifecycle methods shown in step 6, not in &lt;code&gt;onCreate&lt;/code&gt;. On iOS the sample starts the camera as the last line of &lt;code&gt;viewDidLoad&lt;/code&gt;, after &lt;code&gt;play()&lt;/code&gt; and after the effect load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Load an effect
&lt;/h2&gt;

&lt;p&gt;An effect is a folder. Per the &lt;a href="https://docs.banuba.com/far-sdk/effects/overview" rel="noopener noreferrer"&gt;effect structure&lt;/a&gt; docs, only &lt;code&gt;config.json&lt;/code&gt; is mandatory inside it, and the other files are images, shaders, videos and a &lt;code&gt;config.js&lt;/code&gt; script that the effect refers to. The samples ship an effect named &lt;code&gt;TrollGrandma&lt;/code&gt; in an &lt;code&gt;effects&lt;/code&gt; folder, and both platforms load it by name.&lt;/p&gt;

&lt;p&gt;Android loads asynchronously, and passing an empty string unloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;companion&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;MASK_NAME&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"effects/TrollGrandma"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Inside onCreate, on a button:&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="py"&gt;shouldApply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="n"&gt;showMaskButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setOnClickListener&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;shouldApply&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;shouldApply&lt;/span&gt;
    &lt;span class="n"&gt;showMaskButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shouldApply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s"&gt;"Hide mask"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;"Show mask"&lt;/span&gt;
    &lt;span class="c1"&gt;// The mask is loaded asynchronously and applied&lt;/span&gt;
    &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shouldApply&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;MASK_NAME&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;iOS loads synchronously with &lt;code&gt;player?.load(effect: "TrollGrandma")&lt;/code&gt;, as shown in step 4.&lt;/p&gt;

&lt;p&gt;Note the asymmetry in those two strings, because it is a common first bug. Android passes the path &lt;code&gt;effects/TrollGrandma&lt;/code&gt; relative to assets. iOS passes the bare name &lt;code&gt;TrollGrandma&lt;/code&gt;, because the &lt;code&gt;effects&lt;/code&gt; folder is already in the &lt;code&gt;resourcePath&lt;/code&gt; array you set in step 3. Copying the Android constant into the iOS project gives you a silent no-op.&lt;/p&gt;

&lt;p&gt;The decision at this step is where effects come from. You can write the folder yourself starting from a blank &lt;code&gt;config.json&lt;/code&gt;, build one in &lt;a href="https://studio.banuba.com/" rel="noopener noreferrer"&gt;Banuba Studio&lt;/a&gt;, or buy one from the &lt;a href="https://assetstore.banuba.net/" rel="noopener noreferrer"&gt;Banuba Asset Store&lt;/a&gt;. Which packages the effect needs comes back to step 2: a 3D mask needs face tracking only, while lipstick needs the &lt;code&gt;lips&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;Where this step fails: the effect loads and nothing changes on screen, or part of it is missing. That is the missing-package symptom from step 2, and the logs name the package.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Handle permissions, the lifecycle and the device you did not test on
&lt;/h2&gt;

&lt;p&gt;The sample activity extends a &lt;code&gt;BaseActivity&lt;/code&gt; that requests &lt;code&gt;CAMERA&lt;/code&gt; and &lt;code&gt;RECORD_AUDIO&lt;/code&gt; at &lt;code&gt;onStart&lt;/code&gt; and finishes the activity if either is denied. Your app already has a permission flow, so the point is only that the camera device must not start before the grant.&lt;/p&gt;

&lt;p&gt;The lifecycle calls in the Android sample are the part most ports drop. Each object is started, paused and closed in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onStart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// We start the camera and then the player starts taking frames&lt;/span&gt;
    &lt;span class="n"&gt;cameraDevice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onResume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onResume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onPause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onPause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;// Pause the player when the activity is inactive&lt;/span&gt;
    &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onStop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// After this, the camera stops capturing frames and transmitting them to the player&lt;/span&gt;
    &lt;span class="n"&gt;cameraDevice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onStop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// After you are done using the player, free all resources by calling close()&lt;/span&gt;
    &lt;span class="n"&gt;cameraDevice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;surfaceOutput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onDestroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three &lt;code&gt;close()&lt;/code&gt; calls in &lt;code&gt;onDestroy&lt;/code&gt; are what the sample's own comment calls mandatory. A user who backgrounds the app without pressing anything goes through &lt;code&gt;onPause&lt;/code&gt; and &lt;code&gt;onStop&lt;/code&gt;, so the camera is released by that path too.&lt;/p&gt;

&lt;p&gt;The last decision is the device you test on. Banuba's &lt;a href="https://docs.banuba.com/far-sdk/tutorials/capabilities/technical_specification" rel="noopener noreferrer"&gt;technical specification&lt;/a&gt; publishes face tracking figures per device tier, measured under fixed lab conditions, and names no handsets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier, per Banuba's specification&lt;/th&gt;
&lt;th&gt;Single face, FPS&lt;/th&gt;
&lt;th&gt;Max angle, degrees&lt;/th&gt;
&lt;th&gt;Max distance, cm&lt;/th&gt;
&lt;th&gt;5 faces, FPS&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Android Low&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;170&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android High&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;180&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS Mid&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;230&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS High&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;230&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Banuba produced these figures in its own lab, and no third party has repeated them. In text: single-face tracking holds 25 FPS on the Android Low tier and 30 FPS on Android High and both iOS tiers. The maximum tracking angle is 80 degrees on every tier. The maximum tracking distance is 170 cm on Android Low, 180 cm on Android High and 230 cm on both iOS tiers. With five faces in frame, Android Low drops to 22 FPS, Android High to 27, and both iOS tiers stay at 30. The specification lists five as the maximum number of faces tracked with acceptable quality on most devices.&lt;/p&gt;

&lt;p&gt;So the low-tier Android handset is the one that shows the gap between your first effect and the spec sheet. Run the effect you intend to ship on one before you decide the effect is done, and if you want a device list to start from, our tested guide to &lt;a href="https://www.banuba.com/blog/best-ar-sdk-for-android-and-ios" rel="noopener noreferrer"&gt;AR SDKs on Android and iOS&lt;/a&gt; names the handsets we used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six steps, two code files per platform, one device to test on
&lt;/h2&gt;

&lt;p&gt;The integration touches two code files per platform, plus your build files: an application class that initializes the SDK with the token, and a camera screen that connects a camera device to a &lt;code&gt;Player&lt;/code&gt; and an output view, then loads an effect by folder name.&lt;/p&gt;

&lt;p&gt;The costs sit at the edges. Token storage decides whether a renewal is a server change or a store submission. Package selection decides app size and whether a feature silently lacks its neural network. The lifecycle block decides whether the camera is released when the user leaves. And the Android Low tier, at 25 FPS single-face and 22 FPS with five faces in Banuba's own figures, is the device that tells you whether the effect is finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Where is the Banuba Face AR SDK documentation for Android and iOS integration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Banuba publishes the integration docs per platform under one Getting Started page, with Android, iOS, Web, Flutter, React Native and Desktop tabs, and the installation page lists every Banuba Face AR SDK package with what it provides. The same docs ship an LLM-friendly &lt;code&gt;llms-full.txt&lt;/code&gt; file for AI-assisted setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where are the Banuba Face AR SDK code samples?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Banuba keeps platform-specific sample projects on GitHub. The Android and iOS repositories linked in this guide hold the camera sample used above, and Banuba also publishes quickstart repositories for Flutter, React Native, Web, Unity and desktop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when the Banuba Face AR SDK token expires?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For one month after expiry, Banuba Face AR SDK keeps working but displays a Banuba watermark. After that month the SDK functionality stops while the rest of the app works normally. The token cannot be extended; you request a new one and update it in the app, which is why Banuba recommends storing it on a server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Banuba SDK watermark appeared after the trial, what do I do?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A watermark after the trial means the 14-day demo token of Banuba Face AR SDK has expired. Request a commercial token, or a renewed demo token if you have not finished evaluating, and replace the value you pass to &lt;code&gt;BanubaSdkManager.initialize&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Banuba Face AR SDK have Flutter and React Native plugins?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Banuba maintains the Banuba Face AR SDK Flutter plugin on pub.dev and the React Native plugin on npm, and both sit on the same SDK as the native Android and iOS integration shown here.&lt;/p&gt;

</description>
      <category>android</category>
      <category>ios</category>
      <category>tutorial</category>
      <category>augmentedreality</category>
    </item>
    <item>
      <title>6 Routes for Adding a Video Editing SDK to an iOS App and Where Each Breaks</title>
      <dc:creator>Team Banuba</dc:creator>
      <pubDate>Wed, 14 Oct 2026 08:37:14 +0000</pubDate>
      <link>https://dev.to/team_banuba/6-routes-for-adding-a-video-editing-sdk-to-an-ios-app-and-where-each-breaks-1053</link>
      <guid>https://dev.to/team_banuba/6-routes-for-adding-a-video-editing-sdk-to-an-ios-app-and-where-each-breaks-1053</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jdfd5dhpjd9cgkj9j5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9jdfd5dhpjd9cgkj9j5r.png" alt="Cover titled " width="799" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A video editing SDK for iOS is a packaged framework that hands your app a timeline, effects and export so your team does not assemble them from AVFoundation primitives. Six routes are realistic on iOS today, and the right one depends on a single input: the list of things your editor must do on day one.&lt;/p&gt;

&lt;p&gt;The routes fail at different points, from a portrait-only system view to a license that only arrives after a sales call. Banuba sells one of the SDKs compared below, the Banuba Video Editor SDK, and it gets the same treatment as the rest, including the jobs it cannot do and the toolchain it forces on your build machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: List what the editor has to do
&lt;/h2&gt;

&lt;p&gt;Before comparing anything, collect four inputs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your deployment floor and toolchain.&lt;/strong&gt; The minimum iOS version you ship and the Xcode version your CI runs. One SDK below requires Xcode 26.0, which in turn requires a recent macOS, so check your build machines before your deployment target.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your feature tier.&lt;/strong&gt; Sort the plan into one of four buckets: (a) trim, join and export; (b) a timeline UI with text, music and filters; (c) camera-time effects such as face masks, beauty or background replacement applied while recording; (d) the same editor on Android, in a browser or on a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your oldest supported device.&lt;/strong&gt; Step 5 times exports on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who signs contracts.&lt;/strong&gt; Two of the routes cannot be prototyped until someone talks to sales.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tier (a) has two no-cost answers; tiers (b) to (d) are where licensed SDKs come in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Choose a video editing SDK route (or none)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Product compared&lt;/th&gt;
&lt;th&gt;Access and availability&lt;/th&gt;
&lt;th&gt;iOS install path (per vendor docs)&lt;/th&gt;
&lt;th&gt;App size added (vendor-published)&lt;/th&gt;
&lt;th&gt;Trial (vendor terms)&lt;/th&gt;
&lt;th&gt;Where it breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;UIVideoEditorController&lt;/td&gt;
&lt;td&gt;Apple's system trim view in UIKit&lt;/td&gt;
&lt;td&gt;Free, first-party&lt;/td&gt;
&lt;td&gt;Part of UIKit&lt;/td&gt;
&lt;td&gt;Ships with iOS&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;Portrait only; no subclassing or overlay views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AVFoundation&lt;/td&gt;
&lt;td&gt;Apple's media framework&lt;/td&gt;
&lt;td&gt;Free, first-party&lt;/td&gt;
&lt;td&gt;Part of the iOS SDK&lt;/td&gt;
&lt;td&gt;Ships with iOS&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;No editor UI, no face AR, beauty, background removal or captions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Banuba Video Editor SDK&lt;/td&gt;
&lt;td&gt;Native editor with UI plus Face AR&lt;/td&gt;
&lt;td&gt;Open sample on GitHub; trial token on request, then license&lt;/td&gt;
&lt;td&gt;CocoaPods 1.12.1+ or SPM; iOS 15.0+, Swift 5.9+, Xcode 26.0+&lt;/td&gt;
&lt;td&gt;14 MB core, 45 MB full bundle&lt;/td&gt;
&lt;td&gt;14 days, all features&lt;/td&gt;
&lt;td&gt;Requires Xcode 26 on your build machines; no browser editor, no headless server render, no pro multi-track or HDR export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IMG.LY Video Editor SDK (CE.SDK)&lt;/td&gt;
&lt;td&gt;Cross-platform editor engine&lt;/td&gt;
&lt;td&gt;Sales-gated custom quote&lt;/td&gt;
&lt;td&gt;CocoaPods, &lt;code&gt;pod 'IMGLYUI'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;td&gt;30 days, full access&lt;/td&gt;
&lt;td&gt;Each platform licensed separately; no face AR or beauty in documented features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Meishe StreamingSDK + EffectSDK&lt;/td&gt;
&lt;td&gt;Pro editing and effects engine&lt;/td&gt;
&lt;td&gt;Login-gated download, contact sales&lt;/td&gt;
&lt;td&gt;Objective-C against &lt;code&gt;NvsStreamingContext&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Not published&lt;/td&gt;
&lt;td&gt;Trial authorization, watermarked output&lt;/td&gt;
&lt;td&gt;Per-app license file bound to bundle ID; media must be transcoded to its own m3u8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picsart Editor SDK&lt;/td&gt;
&lt;td&gt;Web editor embed&lt;/td&gt;
&lt;td&gt;CDN script tag; no iOS package&lt;/td&gt;
&lt;td&gt;None documented&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;td&gt;Not evaluated&lt;/td&gt;
&lt;td&gt;Desktop browsers only; WebView unsupported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Figures come from each vendor's own documentation as read in September 2026. Vendor pages change, so anything that decides a build (price, size, trial terms, license mechanics) should come from the vendor in writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  UIVideoEditorController
&lt;/h3&gt;

&lt;p&gt;Apple's system interface for trimming and re-encoding a recorded movie, free and first-party. The &lt;a href="https://developer.apple.com/documentation/uikit/uivideoeditorcontroller" rel="noopener noreferrer"&gt;UIKit reference&lt;/a&gt; lists its limits: portrait mode only, no subclassing, a private view hierarchy, no overlay views, and availability only on devices that support video recording. It wins on effort, with zero UI code for trimming.&lt;/p&gt;

&lt;h3&gt;
  
  
  AVFoundation
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/avfoundation" rel="noopener noreferrer"&gt;AVFoundation&lt;/a&gt; is first-party, costs nothing and sits in the iOS SDK, with compositions, tracks and export sessions as building blocks. It wins on cost and dependency count. It has no editor UI and no face AR, beauty, background removal or automatic captions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Banuba Video Editor SDK
&lt;/h3&gt;

&lt;p&gt;A native iOS editor with its own UI, installed via CocoaPods or Swift Package Manager. The &lt;a href="https://www.banuba.com/video-editor-sdk" rel="noopener noreferrer"&gt;Video Editor SDK page&lt;/a&gt; lists transitions, an audio editor, text and GIFs, LUT color filters, templates, and Face AR with face filters and green screen background subtraction. Access starts with a 14-day trial token covering all features, then a license priced on monthly active users with no per-developer-seat fees and no public price list. Of the three licensed SDKs, it is the only one with a published size.&lt;/p&gt;

&lt;p&gt;Two boundaries worth knowing before you add the pod. It does not cover browser editing, headless server rendering, or pro multi-track and HDR work. And it requires Xcode 26.0 or newer, which is the strictest toolchain requirement of the six routes and the one most likely to stop a prototype on day one, since it rules out build machines on an older macOS.&lt;/p&gt;

&lt;h3&gt;
  
  
  IMG.LY Video Editor SDK
&lt;/h3&gt;

&lt;p&gt;Built on the CE.SDK engine, the &lt;a href="https://img.ly/products/video-sdk/" rel="noopener noreferrer"&gt;IMG.LY video SDK&lt;/a&gt; runs on iOS, Android, web, React Native, Flutter, Electron, macOS and Mac Catalyst, plus a Node.js headless renderer. It handles MP4, WebM and MOV, and IMG.LY states GDPR compliance and SOC 2 Type II certification. Access is a custom quote metered by MAU, with each platform licensed separately and AI plugin usage billed by the model providers. It wins on platform reach and on trial length. Face AR and beauty are absent from its documented features, and it does not publish an SDK size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Meishe StreamingSDK
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://en.meishesdk.com/" rel="noopener noreferrer"&gt;Meishe&lt;/a&gt; pairs StreamingSDK with EffectSDK. It wins on editing depth: multi-track timelines, nestable sub-timelines, 4K editing, 8K image processing, HDR export with HLG or ST2084 curves, and 106- or 240-point face landmark models. Its access is the heaviest here: login-gated downloads, contact-sales pricing with no published figure, a per-app license bound to your bundle ID, a watermark until authorization, and 4K licensed separately. No SDK size is published.&lt;/p&gt;

&lt;h3&gt;
  
  
  Picsart Editor SDK
&lt;/h3&gt;

&lt;p&gt;A web embed loaded by a CDN script tag, for desktop browsers only. Mobile browsers are not officially supported and WebView is explicitly unsupported, which rules out wrapping it in an iOS app. It also lacks real-time editing, allows one source video per project, and caps output at 1080p and 10 minutes. We did not evaluate its signup terms because it has no iOS path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: How to trim and export a video with AVFoundation
&lt;/h2&gt;

&lt;p&gt;For tier (a), this is the whole job. The function loads a clip, clamps the end time to the asset's duration, exports the range as MP4 at the highest-quality preset, and throws on each failure path.&lt;/p&gt;

&lt;p&gt;Two export paths appear below, behind an availability check. The newer &lt;code&gt;export(to:as:)&lt;/code&gt; is cleaner, and the completion-handler path is what runs on an iOS 15 deployment target. Confirm the exact availability badge on &lt;a href="https://developer.apple.com/documentation/avfoundation/avassetexportsession" rel="noopener noreferrer"&gt;Apple's AVAssetExportSession reference&lt;/a&gt; against the SDK you build with, and delete the branch if your floor is above it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;AVFoundation&lt;/span&gt;

&lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;TrimExportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;sessionUnavailable&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;mp4NotSupported&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;exportFailed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;trimAndExport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;startSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;endSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;asset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AVURLAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CMTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;startSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;preferredTimescale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CMTimeMinimum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;CMTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;endSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;preferredTimescale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AVAssetExportSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nv"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;presetName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;AVAssetExportPresetHighestQuality&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;TrimExportError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessionUnavailable&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;guard&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;supportedFileTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mp4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;TrimExportError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mp4NotSupported&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeRange&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;CMTimeRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;// Start from an empty destination path.&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;#available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iOS&lt;/span&gt; &lt;span class="mf"&gt;18.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mp4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outputFileType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mp4&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;withCheckedContinuation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;continuation&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exportAsynchronously&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;continuation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;CancellationError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kt"&gt;TrimExportError&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exportFailed&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Caller&lt;/span&gt;
&lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;trimAndExport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inputURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;outputURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Export finished: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;outputURL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Export failed: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;localizedDescription&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;inputURL&lt;/code&gt; and &lt;code&gt;outputURL&lt;/code&gt; are your own file URLs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The export session comes back nil
&lt;/h3&gt;

&lt;p&gt;The initializer is failable. Before blaming the file, call &lt;code&gt;AVAssetExportSession.determineCompatibility(ofExportPreset:with:outputFileType:completionHandler:)&lt;/code&gt; with your asset and preset; Apple provides it for this check.&lt;/p&gt;

&lt;h3&gt;
  
  
  The export throws on the MP4 file type
&lt;/h3&gt;

&lt;p&gt;Apple documents that the export throws when the file type is missing from &lt;code&gt;supportedFileTypes&lt;/code&gt;. The guard in the snippet reports that case as its own error.&lt;/p&gt;

&lt;h3&gt;
  
  
  The export throws after the user leaves the screen
&lt;/h3&gt;

&lt;p&gt;Apple's documentation says the export also ends on cancellation, and that cancelling the parent Task cancels it. If the task is tied to a view's lifetime, dismissing the view ends the export. The legacy branch surfaces the same case as a &lt;code&gt;CancellationError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Past this point, a timeline UI, filter previews and face effects during recording are yours to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: How to integrate a video editor SDK for iOS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Banuba: install, permissions, license check
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://docs.banuba.com/ve-pe-sdk/docs/ios/requirements/" rel="noopener noreferrer"&gt;iOS requirements page&lt;/a&gt; sets the floor, and the &lt;a href="https://docs.banuba.com/ve-pe-sdk/docs/ios/integration-ve" rel="noopener noreferrer"&gt;integration guide&lt;/a&gt; walks through five tasks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the pods with CocoaPods 1.12.1 or newer, or the packages with Swift Package Manager.&lt;/li&gt;
&lt;li&gt;Add four usage strings to &lt;code&gt;Info.plist&lt;/code&gt;: &lt;code&gt;NSCameraUsageDescription&lt;/code&gt;, &lt;code&gt;NSMicrophoneUsageDescription&lt;/code&gt;, &lt;code&gt;NSPhotoLibraryUsageDescription&lt;/code&gt; and &lt;code&gt;NSAppleMusicUsageDescription&lt;/code&gt; (media library).&lt;/li&gt;
&lt;li&gt;Create a module class that builds a &lt;code&gt;VideoEditorConfig&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;BanubaVideoEditor&lt;/code&gt; with your license token.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;getLicenseState&lt;/code&gt; and open the editor only when it reports a valid license.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check the Xcode version on your build machines before step 1. The requirements page lists Xcode 26.0 or newer, and a CI image or a developer laptop on an older macOS will not get there with a pod install.&lt;/p&gt;

&lt;p&gt;Banuba documents integration "in as little as 8 minutes"; that is our own claim, and the runnable &lt;a href="https://github.com/Banuba/ve-sdk-ios-integration-sample" rel="noopener noreferrer"&gt;ve-sdk-ios-integration-sample&lt;/a&gt; is how to test it on your machine. Teams using Claude Code, Codex or Qwen Code can install Banuba's &lt;a href="https://github.com/Banuba/ai-skills" rel="noopener noreferrer"&gt;agent skills&lt;/a&gt; and let the coding agent perform these steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  BanubaVideoEditor returns nil
&lt;/h3&gt;

&lt;p&gt;Per the docs, the initializer returns nil when the token is incorrect, for example empty or truncated. Check how the token reaches the binary first.&lt;/p&gt;

&lt;h3&gt;
  
  
  The editor opens to a "Video content unavailable" screen
&lt;/h3&gt;

&lt;p&gt;The docs state this appears when the SDK starts with a revoked or expired license. The getLicenseState check in task 5 catches it before the user does.&lt;/p&gt;

&lt;h3&gt;
  
  
  IMG.LY: one pod, several licenses
&lt;/h3&gt;

&lt;p&gt;On iOS, IMG.LY installs with &lt;code&gt;pod 'IMGLYUI'&lt;/code&gt; via CocoaPods. The planning work is the license count, since iOS, Android, web and desktop are each licensed separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  Meishe: license file before first frame
&lt;/h3&gt;

&lt;p&gt;Meishe's iOS integration is written in Objective-C against &lt;code&gt;NvsStreamingContext&lt;/code&gt;. Download requires a login, and output stays watermarked until the per-app license, bound to your bundle ID, is authorized. Media must also go via Meishe's transcoding service into its proprietary m3u8 format before it can sit on a track, so plan that step into your import flow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Questions to ask about any client-side license token
&lt;/h3&gt;

&lt;p&gt;The license token ships inside your app, so ask every vendor, including us:&lt;/p&gt;

&lt;p&gt;How is the token scoped to the bundle ID, and what does the SDK do when the bundle ID differs?&lt;br&gt;
What happens if someone lifts the token from the binary and ships it in another app?&lt;br&gt;
How does revocation work, how fast does it reach installed apps, and what does the end user see?&lt;br&gt;
Does license validation need a network call, and what does the SDK do offline?&lt;/p&gt;

&lt;p&gt;Get the answers in writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Measure before you commit
&lt;/h2&gt;

&lt;p&gt;Three numbers settle most decisions, and each has to come from your own build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Binary size delta
&lt;/h3&gt;

&lt;p&gt;Archive the app with and without the SDK and compare. Banuba's requirements docs publish 14 MB for the core editor and 45 MB for the full bundle. IMG.LY and Meishe do not publish a size, so the archive diff is your only number for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Export wall-clock on your oldest device
&lt;/h3&gt;

&lt;p&gt;Time one fixed clip in each route, the AVFoundation snippet included, on the slowest phone you support.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trial window
&lt;/h3&gt;

&lt;p&gt;Banuba gives 14 days with all features, IMG.LY 30 days with full access, and Meishe a trial authorization with watermarked output. Plan the test so it fits the shortest window you are using.&lt;/p&gt;

&lt;p&gt;For more metrics, there is &lt;a href="https://helen-fedulova.hashnode.dev/7-video-editor-sdk-numbers-to-measure-in-your-own-app" rel="noopener noreferrer"&gt;a list of seven numbers worth measuring inside your own app&lt;/a&gt;. If engineering time is the open question, run &lt;a href="https://banuba.medium.com/ios-video-editor-sdk-cost-e32c0bd15fef" rel="noopener noreferrer"&gt;the build-versus-buy&lt;/a&gt; math next to these results.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose between the routes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you are building a trim-and-share feature&lt;/strong&gt;, use AVFoundation, or UIVideoEditorController if a portrait system view is acceptable, because both cost nothing and add nothing to your download.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you are building a creator or social app where face effects run while recording&lt;/strong&gt;, use the Banuba Video Editor SDK, because the timeline UI and Face AR arrive in one dependency on iOS 15+; confirm your build machines can run Xcode 26 first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your editor must also run on the web or render on a server&lt;/strong&gt;, use IMG.LY, because one engine covers web, mobile and a Node.js renderer; budget for one license per platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you need multi-track timelines, 4K editing or HDR export&lt;/strong&gt;, use Meishe, because its editing model goes deepest; start the license conversation early, since output stays watermarked until authorization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the editor lives only in a desktop browser&lt;/strong&gt;, Picsart's Editor SDK is the one that fits there; it is not an option for an iOS app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where each vendor ranks per criterion is covered in our &lt;a href="https://www.banuba.com/blog/best-ios-video-editor-sdks" rel="noopener noreferrer"&gt;iOS SDK comparison&lt;/a&gt; on the Banuba blog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stay on Apple's frameworks until effects or other platforms enter the plan
&lt;/h2&gt;

&lt;p&gt;Tier (a) apps can ship on AVFoundation with the snippet above and no license. Once camera-time effects, a timeline UI, web reach or HDR depth appear on the roadmap, a licensed SDK is the shorter path, and the scenario list names which one. Size and export time measured in your own app confirm it within the trial window.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are the Banuba Video Editor SDK iOS requirements?
&lt;/h3&gt;

&lt;p&gt;iOS 15.0 or newer, Swift 5.9 or newer and Xcode 26.0 or newer, installed through CocoaPods 1.12.1+ or Swift Package Manager. Banuba supports arm64 and arm64e, with limited x86-64 support. The Xcode floor is the one to check first, because it constrains your build machines rather than your users' phones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a free way to add video editing to an iOS app?
&lt;/h3&gt;

&lt;p&gt;Yes. AVFoundation and UIVideoEditorController cost nothing. Among licensed options, Banuba offers a 14-day trial with all features, IMG.LY 30 days with full access, and Meishe a watermarked trial authorization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can the Banuba Video Editor SDK run in a browser or on a server?
&lt;/h3&gt;

&lt;p&gt;No. Banuba covers native iOS and Android plus Flutter and React Native. For browser editing or headless rendering, IMG.LY's CE.SDK is the documented fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  How is the Banuba Video Editor SDK priced compared with IMG.LY and Meishe?
&lt;/h3&gt;

&lt;p&gt;Banuba prices on monthly active users with no per-developer-seat fees and no public price list. IMG.LY quotes per customer, metered by MAU, with each platform licensed separately. Meishe publishes no price and routes buyers to sales.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>swift</category>
      <category>aivideo</category>
      <category>agentskills</category>
    </item>
    <item>
      <title>Virtual Eyewear Try-On: 4 Jobs Between Frame Photos and Cart</title>
      <dc:creator>Team Banuba</dc:creator>
      <pubDate>Mon, 31 Aug 2026 20:44:28 +0000</pubDate>
      <link>https://dev.to/team_banuba/virtual-eyewear-try-on-4-jobs-between-frame-photos-and-cart-8ep</link>
      <guid>https://dev.to/team_banuba/virtual-eyewear-try-on-4-jobs-between-frame-photos-and-cart-8ep</guid>
      <description>&lt;p&gt;Putting eyewear try-on on a storefront breaks into four jobs: turning photographs of each frame into a try-on asset, deciding which browsers get the entry point, mounting the widget and choosing what opens it, and connecting the catalog check to the cart events. Three are front-end work. The first is a data pipeline, and it sets the launch date.&lt;/p&gt;

&lt;p&gt;The ticket usually arrives with a design attached, a button under the add-to-cart block, and no picture of what sits behind it. The code below uses the &lt;a href="https://www.banuba.com/glasses-virtual-try-on" rel="noopener noreferrer"&gt;Banuba virtual try-on widget&lt;/a&gt; because that is what we build, so the attribute and method names are ours, and every fact about them is cited to our own documentation. The four jobs hold for any web try-on engine you mount.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;Four things belong in hand before the first commit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An HTTPS origin for every environment the widget runs in, staging included.&lt;/li&gt;
&lt;li&gt;A merchant ID issued by Banuba, which the widget uses to load your configuration.&lt;/li&gt;
&lt;li&gt;Photographs of every frame in the launch collection, shot to the digitization spec rather than pulled from the web catalog.&lt;/li&gt;
&lt;li&gt;A decision about where the entry point lives on the product detail page, since it drives the gating logic in the second job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Digitizing the frames
&lt;/h2&gt;

&lt;p&gt;Digitization turns photographs of a physical frame into the asset the widget renders on a face, and in the admin dashboard you do it one frame at a time. Per frame you supply a name, a brand picked from the list or created on the spot, a SKU, an optional product page URL, then three images in the try-on model step: left, body and right. A preview renders while the images upload, so a bad set shows itself before you save, &lt;a href="https://tintvto.com/docs/glasses-digitization.html" rel="noopener noreferrer"&gt;as our glasses dashboard guide describes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since widget v1.7.0, released 30 April 2026 according to &lt;a href="https://tintvto.com/docs/changelog.html" rel="noopener noreferrer"&gt;our widget changelog&lt;/a&gt;, that processing runs automatically. The consequence lands on whoever owns the photography. Our docs state that photos failing the requirements give a poor result, and that it cannot be repaired afterwards, because nothing about the processing is manual. Treat the photo spec as an input requirement with the same weight as a schema.&lt;/p&gt;

&lt;p&gt;Here is the part that reshapes a project plan. The bulk CSV path is makeup only. In &lt;a href="https://tintvto.com/docs/bulk_upload.html" rel="noopener noreferrer"&gt;our bulk upload template&lt;/a&gt; the Category column always takes the value MAKEUP, and the columns after it are Region, Finish, Color as a 6-digit hex, and Coverage, all parameters of a cosmetic product. Frames therefore go in through the Glasses tab individually, and our docs mark that tab Beta. For a catalog of any size that is a data entry line with a person's name on it, and sizing it comes before anyone commits to a launch date.&lt;/p&gt;

&lt;p&gt;Production work asks for more than the three try-on images. The commercial list in &lt;a href="https://tintvto.com/docs/knowledge-base.html" rel="noopener noreferrer"&gt;our knowledge base&lt;/a&gt; adds UPC number, a storefront image link, the product page URL, photos from multiple angles, and a 3D model, with frame size where it is known. Collect all of it in the same pass, since walking the collection twice costs the same hours twice.&lt;/p&gt;

&lt;p&gt;The decision point is ownership. Someone has to supply the photographs, either merchandising from existing shoots or a studio from a new one, and that answer moves the timeline more than any code in this article does. Pull one frame record out of your PIM as well and see whether the frame size fields exist there at all.&lt;/p&gt;

&lt;p&gt;What goes wrong here is scheduling. A team scopes the work as a front-end sprint, then finds in week one that the launch collection has to be entered by hand and reshot on top, because the existing photography was styled for hero banners at an angle the try-on step cannot use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deciding which browsers get the button
&lt;/h2&gt;

&lt;p&gt;The try-on button should render off a runtime check rather than a static support table, because camera access has hard requirements a shopper's browser either satisfies or fails. The first is transport. Webcam access needs an HTTPS/TLS connection, so a staging box on plain HTTP fails at the first call and looks exactly like a broken widget; MDN documents &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" rel="noopener noreferrer"&gt;the secure context rule behind getUserMedia&lt;/a&gt;, which applies to every camera API on the page, ours included.&lt;/p&gt;

&lt;p&gt;The second is the engine. &lt;a href="https://tintvto.com/docs/introduction.html" rel="noopener noreferrer"&gt;Our documented system requirements&lt;/a&gt; specify the last 2 Safari major versions, the last 2 iOS major versions, and the last 5 versions of Chrome, Opera, Edge and Samsung Internet. Read that as the set we test against. A version floor describes a population, while the shopper in front of you is one device with one driver and one browser build.&lt;/p&gt;

&lt;p&gt;The answer for an individual shopper comes from feature detection at render time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cameraIsAvailable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isSecureContext&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getUserMedia&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;cameraIsAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;window.isSecureContext covers transport, navigator.mediaDevices is absent entirely on browsers without media capture, and tryOnButton is the entry point your PDP template renders. The button now appears only where both conditions hold.&lt;/p&gt;

&lt;p&gt;Then comes the branch, and both sides are defensible. Hiding the entry point on a failing check keeps the page honest, at the cost of a shopper who never learns the feature exists. Rendering it with a static model photo behind it keeps the feature discoverable. Eyewear weakens that second option in a way a cosmetic shade does not: a frame on a stock model shows how the style reads, while the question that made the shopper tap was how it sits on their own face.&lt;/p&gt;

&lt;p&gt;Skip the gate and the failure is uniform. Every shopper sees the button, and the ones on an old in-app browser or an HTTP staging link get a modal that opens onto nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mounting the widget and picking the trigger
&lt;/h2&gt;

&lt;p&gt;Mounting is two lines of markup, and the judgment call here is what opens the widget afterwards. The bundle loads from a CDN, and the element goes wherever the experience should appear, both covered in &lt;a href="https://tintvto.com/docs/integration.html" rel="noopener noreferrer"&gt;our integration guide&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://tintvto.com/widget.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That path carries no version segment, so it resolves to the current release. Confirm a pinned URL with your technical manager before production, because a minor release can change behavior on a page you have not deployed to in months.&lt;/p&gt;

&lt;p&gt;On a product detail page you want the widget focused on the frame being viewed, which is what the isolated SKU attributes do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;tint-vto&lt;/span&gt; &lt;span class="na"&gt;merchant-id=&lt;/span&gt;&lt;span class="s"&gt;"YOUR_MERCHANT_ID"&lt;/span&gt; &lt;span class="na"&gt;isolated-sku&lt;/span&gt; &lt;span class="na"&gt;sku=&lt;/span&gt;&lt;span class="s"&gt;"8028997081552"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/tint-vto&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"open-vto"&lt;/span&gt; &lt;span class="na"&gt;data-sku=&lt;/span&gt;&lt;span class="s"&gt;"8028997081552"&lt;/span&gt; &lt;span class="na"&gt;hidden&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Try these on&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;merchant-id is the identifier Banuba issues you, isolated-sku switches the widget into single product mode, and sku names the frame on this page. The token and sdk-token attributes are documented as deprecated and slated for removal, so check any snippet inherited from an older sample.&lt;/p&gt;

&lt;p&gt;That merchant ID ships in your page source and any visitor can read it in devtools, which is normal for a widget that loads its configuration client side and is also the first question a security reviewer will ask. The answer worth having in writing before launch is how the identifier is scoped: which origins it works from, how that list is configured, and what happens when someone copies it onto their own domain. Put those three questions to your technical manager during evaluation rather than the week before release.&lt;/p&gt;

&lt;p&gt;Our integration guide documents launch triggers for opening immediately, opening on the window load event, and opening on a customer action. A product detail page wants the third, so that the camera permission prompt follows an explicit tap and reads as an answer to something the shopper asked for.&lt;/p&gt;

&lt;p&gt;The camera itself can be declined, and the fallback is two method calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tint-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useWebcam&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelPhoto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/media/model-front.webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelPhoto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;useWebcam() resolves with the active stream, or with null when the shopper denies access, and useImage() accepts a Blob or File to render on instead. usePhoto() is a deprecated alias of useImage(). On a frames PDP that fallback stays a courtesy, for the fit reason above.&lt;/p&gt;

&lt;p&gt;Two things break here in practice. One is the inherited snippet still passing token, which keeps working until the release that removes it. The other is a widget wired to open on page load, which spends the camera prompt before the shopper has expressed any interest and trains them to dismiss it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting the catalog and the cart
&lt;/h2&gt;

&lt;p&gt;Two wires finish the integration: a catalog check deciding whether the button renders for this frame, and the events keeping the cart in step with the widget. The check is isCustomerSkuExist(customerSkuIds: string[]), which resolves true when every SKU you pass exists in the try-on catalog and can be called before open(), per &lt;a href="https://tintvto.com/docs/widget-api.html" rel="noopener noreferrer"&gt;our widget API reference&lt;/a&gt;. Put it in the PDP render path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tint-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;open-vto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentSku&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isDigitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isCustomerSkuExist&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;currentSku&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;tryOnButton&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isDigitized&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;cameraIsAvailable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/script&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;currentSku reads the data-sku attribute your template wrote onto the button, and cameraIsAvailable() is the detection function from the previous job, imported from wherever you keep it. A frame with no try-on asset yet never shows a button. &lt;/p&gt;

&lt;p&gt;That gate carries more weight for eyewear than for a cosmetic shade, where a missing asset still leaves a swatch and a product photo to look at.&lt;/p&gt;

&lt;p&gt;Cart sync runs on the events the widget emits. Attach the listeners once, wherever you mount the element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storefrontCart&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// your own cart module&lt;/span&gt;

&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addToCart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;removeFromCart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nx"&gt;widget&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redirectToCart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/cart&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our API reference documents detail as an array of Product objects on the first two events, and as null on redirectToCart, which fires when the shopper clicks Go to cart. To drive the widget from your own product data instead, applyProduct() takes fully defined product objects and applyProductByCustomerSku() takes your customer facing SKU identifiers.&lt;/p&gt;

&lt;p&gt;For device checks, widget v1.7.1 of 18 May 2026 added try-on links and QR codes generated per product or for a whole catalog, per the same changelog. QA becomes scanning a code: open it on the phones your analytics say people shop from, and walk the flow inside the Instagram and Facebook in-app browsers, with no deploy behind any of it. Rendering and camera behavior diverge across handsets much further than a support matrix suggests, which is what &lt;a href="https://www.banuba.com/blog/best-web-ar-apis-tested" rel="noopener noreferrer"&gt;our web AR testing kept running into&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The defects to watch for are quiet ones. Leave any of those events unhandled and the cart drifts, so a shopper adds a frame inside the try-on and finds the header count unchanged. Ship a frame to the PDP with a live button before anyone has processed its photos and you get the ungated version of the check above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What carries over to another engine
&lt;/h2&gt;

&lt;p&gt;Swap the vendor and the identifiers change while the shapes survive. There is always an asset preparation step with a photography spec attached, and it is the long pole. A capability gate follows it, since camera access on the web is conditional everywhere. Then a mount point plus a trigger decision, where the case for the tap is the permission prompt. Last comes the boundary between the try-on surface and your cart, crossed by events one way and product identifiers the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;The three front-end jobs are a normal week for someone holding the merchant ID and a PDP template. The variable is the photography, and better code will not compress it, because it is bounded by how fast someone shoots frames to spec and enters them one at a time through a Beta tab. So run the estimate in that order: count the frames in the launch collection, find out who is shooting them and whether the UPC and frame size fields exist in your PIM today, and put the code work after that number.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What do you upload for each frame?
&lt;/h3&gt;

&lt;p&gt;A name, a brand, a SKU, an optional product page URL, and three images of the frame: left, body and right. Going to production, collect the commercial set in the same pass, which adds UPC number, a storefront image link, photos from multiple angles, a 3D model, and frame size where you have it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can frames be bulk imported from a CSV?
&lt;/h3&gt;

&lt;p&gt;No. The bulk upload template is makeup only: its Category field takes the single value MAKEUP and its parameter columns are Region, Finish, Color and Coverage. Frames go in individually through the Glasses tab of the admin dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the try-on work locally but not on my staging server?
&lt;/h3&gt;

&lt;p&gt;Almost always because staging is served over plain HTTP. Browsers restrict camera access to secure contexts, and localhost counts as one while &lt;a href="http://staging.example.com" rel="noopener noreferrer"&gt;http://staging.example.com&lt;/a&gt; does not. Put a certificate on staging and the same build works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does eyewear try-on need a native app?
&lt;/h3&gt;

&lt;p&gt;No. The widget is web based, loads from a CDN script tag, and runs in the browser on desktop and mobile, so no app store release sits in the critical path.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens on an unsupported browser?
&lt;/h3&gt;

&lt;p&gt;The widget opens and the camera call fails, which reads to the shopper as a broken page. Hence the feature detection in the second job: the entry point renders only where a secure context and a media devices API are both present.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which attributes should a new integration use?
&lt;/h3&gt;

&lt;p&gt;merchant-id, plus isolated-sku and sku on a product detail page. token and sdk-token are deprecated and scheduled for removal, and usePhoto() is a deprecated alias of useImage().&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ecommerce</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>6 AR Makeup Try-On Failures a React Storefront Hits in Production</title>
      <dc:creator>Team Banuba</dc:creator>
      <pubDate>Wed, 19 Aug 2026 09:37:09 +0000</pubDate>
      <link>https://dev.to/team_banuba/6-ar-makeup-try-on-failures-a-react-storefront-hits-in-production-1e07</link>
      <guid>https://dev.to/team_banuba/6-ar-makeup-try-on-failures-a-react-storefront-hits-in-production-1e07</guid>
      <description>&lt;p&gt;AR makeup try-on rarely fails in the demo. It fails on the third page load, on a mid-range Android, in the Instagram in-app browser, or on a skin tone nobody on the team has. Six failure modes account for most of it: camera permission handling, double-mounted effects, shade fidelity, frame rate collapse, SKU mapping drift, and mobile browser policy.&lt;/p&gt;

&lt;p&gt;Each one below has the same shape: what actually breaks, why React or the mobile browser makes it worse, and the test you can run before a shopper finds it. &lt;/p&gt;

&lt;p&gt;The code examples use the &lt;a href="https://tintvto.com/docs/introduction.html" rel="noopener noreferrer"&gt;Banuba virtual try-on widget&lt;/a&gt; because that is what we build, but every test method here is vendor-neutral. If you are integrating a different engine, the checks still apply.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc36x1dvz5zhvy6cb6e8i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc36x1dvz5zhvy6cb6e8i.png" alt="banuba makeup virtual try-on widget preview on the frontend" width="799" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does a try-on demo pass and the storefront fail?
&lt;/h2&gt;

&lt;p&gt;A vendor demo runs one product, one browser, one lighting setup, and one face. A storefront runs your whole catalog across every device your analytics has ever seen. The gap between those two is where the six failures live.&lt;/p&gt;

&lt;p&gt;It is worth being precise about what a web try-on actually is before testing it. It is a camera stream, a face tracking model, a rendering layer, and a mapping from your SKUs to render parameters. Three of the six failures below are in code you wrote, not in the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. What happens when the shopper denies camera access?
&lt;/h2&gt;

&lt;p&gt;This is the most common one and the cheapest to fix. getUserMedia can be denied, dismissed, blocked by enterprise policy, or unavailable because the page is not on a secure origin. Per MDN, getUserMedia is only available in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia" rel="noopener noreferrer"&gt;secure context&lt;/a&gt;, meaning HTTPS or localhost, so a staging box on plain HTTP will fail every time and look like a bug in the widget.&lt;/p&gt;

&lt;p&gt;A try-on with no fallback shows a black rectangle and the shopper leaves the page. A try-on with a fallback shows the product on a model photo and keeps the session alive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function openTryOn(widget) {
  await widget.open();

 const stream = await widget.useWebcam();
  if (!stream) {
    // Camera denied, dismissed, or unavailable. Do not leave the modal empty.
    const model = await fetch("/model.webp").then((r) =&amp;gt; r.blob());
    await widget.useImage(model);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;In Chrome DevTools, open the three-dot menu, go to More tools, then Sensors, and block camera permission for the origin. Then repeat on plain HTTP. Both paths must render something. Track the denial rate as an event, because if a third of your sessions never get a camera, the fallback image is your real try-on experience and deserves the same design attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Why does the camera light stay on after the modal closes?
&lt;/h2&gt;

&lt;p&gt;This one is specific to React and it is the failure most teams ship without noticing. React 18 in development deliberately mounts, unmounts, and remounts every component. The React documentation is blunt about the reason: Strict Mode calls effects twice so that "bugs caused by missing cleanup logic" surface in development instead of production.&lt;/p&gt;

&lt;p&gt;If your effect opens the widget and never releases it, you get two widget instances and two camera streams. In development the symptom is a doubled modal. &lt;/p&gt;

&lt;p&gt;In production the symptom is worse: the camera indicator stays lit after the shopper closes the try-on, which reads as spyware and generates support tickets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useEffect, useRef } from "react";
export function TryOn({ merchantId, sku }) {
  const ref = useRef(null);

  useEffect(() =&amp;gt; {
    const widget = ref.current;
    let cancelled = false;

    (async () =&amp;gt; {
      await widget.open();
      if (cancelled) await widget.close();
    })();

    return () =&amp;gt; {
      cancelled = true;
      widget.close();          // release the stream
    };
  }, [sku]);

  return &amp;lt;tint-vto ref={ref} merchant-id={merchantId} isolated-sku sku={sku} /&amp;gt;;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Open the try-on, close it, and watch the browser tab's camera indicator. It must go dark within a second. Then navigate between two product pages five times and confirm you still have exactly one widget instance in the DOM. Run this with Strict Mode on, not off. Turning Strict Mode off to make the double mount go away hides the bug rather than fixing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Does the shade look right on every skin tone, or only on yours?
&lt;/h2&gt;

&lt;p&gt;This is the failure that costs money rather than support tickets, and it is the hardest to catch because the team testing it is usually not diverse enough to catch it. &lt;/p&gt;

&lt;p&gt;The single most common complaint reported by virtual try-on buyers is filed, almost word for word, as "product color not displayed correctly": the shade on screen does not match the shade in the box. A naive color overlay washes out on deep skin and looks artificial on light skin, so the same lipstick reads as two different products depending on who is looking at it.&lt;/p&gt;

&lt;p&gt;The fix is not something you test by eye once. Build a fixed test matrix and run it every release. The dermatology standard for this is the Fitzpatrick scale, which defines six skin phototypes, and it gives you a defensible set of rows rather than an arbitrary one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtqmnc4iluvjnsbub0da.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwtqmnc4iluvjnsbub0da.jpg" alt="the rable comparing test axis (skin tone, lighting, finish, capture) versus minimum coverage" width="629" height="208"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Written out rather than only in the table: cover all six Fitzpatrick phototypes, four lighting conditions including backlit, at least four finishes including one high-coverage foundation, and both the live webcam path and the uploaded photo path. Twenty-four to forty-eight screenshots per release is a small price against a shade that only works on half your customers.&lt;/p&gt;

&lt;p&gt;On the engine side, skin-tone-aware rendering is the specific capability that separates a try-on from a filter. The &lt;a href="https://www.banuba.com/tint-makeup-virtual-try-on" rel="noopener noreferrer"&gt;Banuba virtual try-on widget&lt;/a&gt; applies 16 makeup product types with skin-tone-aware application for exactly this reason, and supports 16 or more product categories including eyewear, hair color, contacts, jewelry, and accessories. &lt;/p&gt;

&lt;p&gt;Whichever engine you pick, ask for its output across your matrix before you sign, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What is your frame rate on the cheapest phone in your analytics?
&lt;/h2&gt;

&lt;p&gt;Real-time face tracking plus rendering is a per-frame GPU and CPU budget. On a flagship phone you will never see the ceiling. On a three-year-old midrange Android the frame rate drops, the render lags behind the face by a few frames, and the try-on stops feeling like a mirror.&lt;/p&gt;

&lt;p&gt;Pick a floor and enforce it. A practical one is 24 frames per second sustained over sixty seconds, since that is where motion stops reading as stuttering to most people. &lt;/p&gt;

&lt;p&gt;Measure sustained rather than peak, because thermal throttling is what actually gets you: the first ten seconds look fine and second thirty do not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let frames = 0;
const start = performance.now();

function tick() {
  frames += 1;
  if (performance.now() - start &amp;lt; 60000) requestAnimationFrame(tick);
  else console.log("sustained fps:", frames / 60);
}
requestAnimationFrame(tick);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;run it on a real device, not the emulator, and not immediately after a charge. Pull the three most common device models out of your analytics and test the slowest one. If you only have flagships in the office, a cheap prepaid Android is the highest-value QA hardware you can buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Why does one SKU render as a generic tint?
&lt;/h2&gt;

&lt;p&gt;Try-on engines do not render your product. They render the parameters someone mapped to your product: a region, a color, a finish, a coverage level. &lt;/p&gt;

&lt;p&gt;When merchandising adds a shade and nobody digitizes it, the try-on either shows nothing or falls back to something approximate, and the shopper sees a color that is not the one on the box.&lt;/p&gt;

&lt;p&gt;This is a data integrity problem, so treat it as one. Add a contract test in CI that walks your live catalog and asserts every purchasable SKU exists on the try-on side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Fails the build when merchandising adds a SKU that nobody digitized.
const skus = await getPurchasableSkus();          // your catalog
const ok = await widget.isCustomerSkuExist(skus); // try-on catalog
if (!ok) throw new Error("SKU present in catalog but missing from try-on");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That isCustomerSkuExist call is from the Banuba widget API, and most engines expose an equivalent lookup; the &lt;a href="https://github.com/Banuba/beauty-web" rel="noopener noreferrer"&gt;open web sample&lt;/a&gt; has a minimal page you can diff your own wiring against.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Run that check nightly against production data, not just on the fixture set in your test suite. Fixtures never drift. Catalogs always do. Then decide the product behavior for a missing SKU: hiding the try-on button is almost always better than opening a try-on that shows the wrong color.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Does it work inside the Instagram browser?
&lt;/h2&gt;

&lt;p&gt;A large share of beauty traffic arrives from social, which means the page opens in an in-app webview rather than Safari or Chrome. &lt;/p&gt;

&lt;p&gt;Those webviews have their own camera permission behavior, and iOS has a long-standing quirk that catches teams out: WebKit requires the playsinline attribute on a video element, otherwise the stream takes over the whole screen in fullscreen playback instead of rendering in place.&lt;/p&gt;

&lt;p&gt;If you embed the try-on in your own native app rather than the mobile web, the same constraint applies from the other direction. Camera access inside a webview needs a real HTTPS origin, so loading inline HTML without setting a proper base URL will fail even though the identical markup works in a browser tab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Android WebView: JS on, and a real HTTPS base URL or the camera never opens.
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.loadDataWithBaseURL("https://shop.example.com/", html, "text/html", "UTF-8", null)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to test it
&lt;/h3&gt;

&lt;p&gt;Send yourself the product URL through Instagram, Facebook, and TikTok on both iOS and Android, and open it from inside each app. Do not test by pasting the link into Safari. That is a different browser with different rules, and it is not where your traffic is.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does a release checklist look like?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pulling the six together, the pre-release pass is short:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Camera denied and plain-HTTP origin both render a fallback, not a black box.&lt;/li&gt;
&lt;li&gt;Camera indicator goes dark within a second of closing, with Strict Mode on.&lt;/li&gt;
&lt;li&gt;Shade matrix screenshots taken across six phototypes and four lighting setups.&lt;/li&gt;
&lt;li&gt;Sustained 24 fps over sixty seconds on the slowest device in your top three.&lt;/li&gt;
&lt;li&gt;Nightly SKU contract test green against production catalog data.&lt;/li&gt;
&lt;li&gt;Product URL opened from inside Instagram on iOS and Android.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the integration itself, the reference worth having open alongside the checklist is the &lt;a href="https://tintvto.com/docs/integration.html" rel="noopener noreferrer"&gt;widget integration guide&lt;/a&gt;, which documents the element, its attributes, and the lifecycle methods used above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;The engineering work in AR makeup try-on has moved. Face tracking and rendering are the vendor's problem now, and a first web integration is realistically a two-week front-end task rather than a computer vision project. &lt;/p&gt;

&lt;p&gt;What has not moved is everything around it: permissions, component lifecycle, device spread, catalog integrity, and the browsers your customers actually use.&lt;/p&gt;

&lt;p&gt;Those six are yours regardless of which engine you integrate, and they are the difference between a try-on that converts and one that quietly loses sessions. Build the test matrix before the launch, not after the first support ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need a native app to run AR makeup try-on?
&lt;/h3&gt;

&lt;p&gt;No. A web-based try-on loads as a script bundle and a custom HTML element on your existing product page, so it runs in the mobile browser without a separate app. A native app can embed the same widget through a webview when you want it inside an existing app experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does the try-on work on desktop but not on my staging server?
&lt;/h3&gt;

&lt;p&gt;Almost always the secure context rule. Browsers only expose getUserMedia over HTTPS or localhost, so a staging box served over plain HTTP will never get a camera stream no matter how correct the integration is.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I test shade accuracy without a diverse test team?
&lt;/h3&gt;

&lt;p&gt;Use a fixed matrix instead of ad hoc checks. Six Fitzpatrick phototypes against four lighting conditions gives you 24 screenshots per finish, which you can collect from a stock photo set for the upload path and from contractors or colleagues for the live path. The point is that it is repeatable between releases.&lt;/p&gt;

&lt;h3&gt;
  
  
  What frame rate is good enough?
&lt;/h3&gt;

&lt;p&gt;Set a floor rather than chasing a number. Sustained 24 fps over a full minute on your slowest common device is a reasonable bar, because motion below that starts reading as stutter and thermal throttling shows up after the first few seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can one integration cover more than makeup?
&lt;/h3&gt;

&lt;p&gt;Yes, if the engine supports it. The Banuba widget covers 16 or more product categories including eyewear, hair color, contacts, jewelry, and accessories, so a storefront that integrates for lipstick can enable additional categories later through configuration rather than a second integration.&lt;/p&gt;

&lt;h3&gt;
  
  
  What breaks most often after launch?
&lt;/h3&gt;

&lt;p&gt;Catalog drift. The integration is static once it ships, but merchandising keeps adding SKUs, and any SKU that is not digitized on the try-on side either disappears or renders approximately. A nightly contract test catches it before a customer does.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>testing</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
