# Changelog

## V2.1.9-E(2026-5-25)

- **Phone SDK version**: `com.rokid.security:phone.sdk:2.1.9-E`
- **Glasses SDK version**: `com.rokid.security:glass3.open.sdk:2.1.9-E`
- **Recommended OTA version**: `1.17.e002-20260509-150201` or later

### Glasses SDK API Changes

#### 1. Event Interception APIs

###### 1.1 `GlassSdk.setPrimaryEventListener()`

**Location**: `GlassSdk.kt`

```kotlin
@JvmStatic
fun setPrimaryEventListener(listener: DeviceEventListener?)
```

**Description**:

- Set the primary device event listener
- The SDK internally holds a single DeviceEventListener slot
- Listeners registered through this method receive all events except those intercepted by Leqi

**Use Case**: Use this when you need to listen for device events (such as keys and battery) without being affected by the Leqi interceptor

###### 1.2 `GlassSdk.registerLeqiInterceptor()`

**Location**: `GlassSdk.kt`

```kotlin
@JvmStatic
fun registerLeqiInterceptor(context: Context, interceptor: LeqiInterceptor)
```

**Description**:

- Register the Leqi voice command interceptor
- The interceptor takes effect when the app is in the foreground and automatically becomes inactive after the app moves to the background
- Call this in Activity.onCreate and call unregisterLeqiInterceptor() in onDestroy

**Parameters**:

- `context`: Android Context, used for lifecycle management
- `interceptor`: interceptor implementation

**Usage Example**:

```kotlin
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GlassSdk.registerLeqiInterceptor(this, object : LeqiInterceptor {
            override fun onLeqiInstruction(extra: String?): Boolean {
                // Return true to intercept, false to allow it through
                return handleCustomInstruction(extra)
            }
        })
    }

    override fun onDestroy() {
        GlassSdk.unregisterLeqiInterceptor()
        super.onDestroy()
    }
}
```

###### 1.3 `GlassSdk.unregisterLeqiInterceptor()`

**Location**: `GlassSdk.kt`

```kotlin
@JvmStatic
fun unregisterLeqiInterceptor()
```

**Description**:

- Unregister the Leqi voice command interceptor
- Usually called in Activity.onDestroy

###### 1.4 `LeqiInterceptor` Interface

**Location**: `LeqiInterceptor.kt` (new file)

```kotlin
interface LeqiInterceptor {
    /**
     * Leqi voice command callback
     * @param extra Extra parameters (JSON format)
     * @return true means intercept this command; false means allow it through
     */
    fun onLeqiInstruction(extra: String?): Boolean
}
```

**Description**:

- Defines the Leqi voice command interceptor interface
- Apps can implement this interface to customize voice command handling logic

#### 2. CameraShareHelper Configuration Switching

###### 2.1 `CameraShareHelper.restartSurfaceWithConfig()`

**Location**: `CameraShareHelper.kt`

```kotlin
fun restartSurfaceWithConfig(config: CameraShareConfig, callback: SurfaceCallback)
```

**Description**:

- **Important**: Switch configuration under the same callbackId to avoid multiple residual Surface sessions in system-server caused by creating new Helpers
- Internally calls `releaseSurface()` first, then `initSurfaceWithConfig()`
- **Must be called on the GL thread**

**Parameters**:

- `config`: New camera configuration (resolution, frame rate, stabilization, zoom)
- `callback`: Callback interface

**Use Case**:

- Dynamically switch camera resolution or frame rate
- Avoid memory leaks caused by creating multiple Helper instances

**Usage Example**:

```kotlin
// Initial configuration
val config1 = CameraShareConfig(
    previewWidth = 1280,
    previewHeight = 720,
    previewTargetFps = 15,
    zoomLevel = 1
)
helper.initSurfaceWithConfig(config1, callback)

// Switch to the new configuration (reuse the same helper)
val config2 = CameraShareConfig(
    previewWidth = 1920,
    previewHeight = 1080,
    previewTargetFps = 24,
    zoomLevel = 2
)
helper.restartSurfaceWithConfig(config2, callback)
```

**Notes**:

- ⚠️ Must be called on the GL thread (the same thread as initSurfaceWithConfig)
- ⚠️ Do not create multiple CameraShareHelper instances to switch configurations

###### 2.2 `CameraShareHelper.restartNv21ExportWithConfig()`

**Location**: `CameraShareHelper.kt`

```kotlin
fun restartNv21ExportWithConfig(enableMix: Boolean, config: CameraShareConfig, callback: Nv21Callback)
```

**Description**:

- Switch NV21 export configuration under the same callbackId to avoid residual sessions from multiple Helpers
- Internally calls `releaseNv21Export()` first, then `initNv21ExportWithConfig()`

**Parameters**:

- `enableMix`: Whether to enable Mix mode (camera + screen overlay)
- `config`: New camera configuration
- `callback`: NV21 callback interface

**Use Case**:

- Dynamically switch the resolution or frame rate of NV21 export
- Avoid memory leaks caused by creating multiple Helper instances

#### 3. Status Query APIs

###### 3.1 `CameraShareHelper.isSurfaceActive()`

**Location**: `CameraShareHelper.kt`

```kotlin
fun isSurfaceActive(): Boolean
```

**Description**:

- Check whether Surface sharing is active
- Returns true if Surface has been initialized and not released

**Use Case**:

- Check the current state before switching configuration
- Avoid repeated initialization or release

###### 3.2 `CameraShareHelper.isNv21Active()`

**Location**: `CameraShareHelper.kt`

```kotlin
fun isNv21Active(): Boolean
```

**Description**:

- Check whether NV21 export is active
- Returns true if NV21 export has been initialized and not released

**Use Case**:

- Check the current state before switching configuration
- State check during resource cleanup

#### 4. Important Notes

###### 1. APIs Related to Memory Leak Fixes

**Background**: Previous versions had a memory leak issue caused by repeatedly creating Surface sessions

**Solution**:

- Added `restartSurfaceWithConfig()` and `restartNv21ExportWithConfig()`
- These two methods reuse the session under the same callbackId to avoid creating multiple sessions

**Migration Recommendation**:

```kotlin
// Old approach (causes memory leaks)
var helper = CameraShareHelper()
helper.initSurfaceWithConfig(config1, callback)
// ... when configuration needs to be switched
helper.releaseSurface()
helper = CameraShareHelper()  // create a new instance <- the problem
helper.initSurfaceWithConfig(config2, callback)

// New approach (recommended)
val helper = CameraShareHelper()
helper.initSurfaceWithConfig(config1, callback)
// ... when configuration needs to be switched
helper.restartSurfaceWithConfig(config2, callback)  // reuse helper
```

###### 2. Notes for Using the Leqi Interceptor

**Lifecycle Management**:

- The interceptor is active only when the app is in the foreground
- LifecycleOwner manages it automatically; manual start/stop is not required
- However, calling `unregisterLeqiInterceptor()` in onDestroy is recommended to ensure cleanup

**Interception Logic**:

```kotlin
GlassSdk.registerLeqiInterceptor(this, object : LeqiInterceptor {
    override fun onLeqiInstruction(extra: String?): Boolean {
        return try {
            val json = JSONObject(extra ?: "{}")
            val command = json.optString("command")

            when (command) {
                "take_photo" -> {
                    // Custom photo capture logic
                    takePhoto()
                    true  // Intercept and prevent Leqi from handling it
                }
                else -> false  // Do not intercept; let Leqi handle it
            }
        } catch (e: Exception) {
            false  // Parsing failed; do not intercept
        }
    }
})
```

###### 3. Using Configuration Change Callbacks

Although no new callback methods are added in this release, existing callbacks are now triggered correctly:

**SurfaceCallback**:

```kotlin
object : CameraShareHelper.SurfaceCallback {
    override fun onSurfaceShareConfigChanged(
        width: Int,
        height: Int,
        appliedPreviewFps: Int,
        videoStabilizationEnabled: Boolean
    ) {
        // Automatically called when configuration changes
        Log.d(TAG, "Config changed: ${width}x${height}@${appliedPreviewFps}fps")
    }

    override fun onZoomLevelChanged(zoomLevel: Int) {
        // Called when the zoom level changes
        Log.d(TAG, "Zoom changed: $zoomLevel")
    }
}
```

**Nv21Callback**:

```kotlin
object : CameraShareHelper.Nv21Callback {
    override fun onNv21ExportResolutionChanged(
        width: Int,
        height: Int,
        appliedPreviewFps: Int
    ) {
        // Called when the NV21 export resolution changes
    }

    override fun onNv21ExportRuntimeParamsChanged(
        appliedPreviewFps: Int,
        videoStabilizationEnabled: Boolean
    ) {
        // Called when runtime parameters change
    }

    override fun onZoomLevelChanged(zoomLevel: Int) {
        // Called when the zoom level changes
    }
}
```

## V2.1.8-E(2026-5-9)

- **Phone SDK version**: `com.rokid.security:phone.sdk:2.1.8-E`
- **Glasses SDK version**: `com.rokid.security:glass3.open.sdk:2.1.8-E-20260518.101638-5`
- **Recommended OTA version**: `1.17.e002-20260509-150201` or later

### Phone SDK API Changes

#### Wi-Fi P2P H.264 Decode Control (Core Change)

When the phone receives H.264, it still automatically converts to NV21 by default. External callers can disable this through the SDK.
The following APIs are added to `IWifiP2PClientOperate`:

```kotlin
// Set whether H.264 is automatically decoded to NV21 (enabled by default)
fun setAutoDecodeH264ToNv21(enable: Boolean)

// Query whether automatic H.264-to-NV21 decoding is currently enabled
fun isAutoDecodeH264ToNv21Enabled(): Boolean
```

**Feature Description**:

- **Default behavior**: Maintains backward compatibility. Automatic H.264-to-NV21 decoding is enabled by default
- **Performance optimization**: When the app only needs H.264 data, automatic decoding can be disabled to save CPU and memory
- **Flexible control**: External callers can dynamically enable or disable it based on actual needs

**Use Case**:

```kotlin
// Scenario 1: Only the H.264 stream is needed for network transmission; disable automatic decoding
wifiP2PClientService?.setAutoDecodeH264ToNv21(false)

// Scenario 2: NV21 data is needed for AI recognition; enable automatic decoding (default)
wifiP2PClientService?.setAutoDecodeH264ToNv21(true)
```

#### GlassVideoStreamParam Data Class

```kotlin
class GlassVideoStreamParam{
    var fps = 20
    var bitrate = 5184000
    /** When true, this video stream uses AR overlay encoding (consistent with USE_AR_MIX on the glasses side). Disabled by default */
    var isARMixEnabled = false
    /** Expected video stream width. -1 indicates using the default glasses-side resolution */
    var width = -1
    /** Expected video stream height. -1 indicates using the default glasses-side resolution */
    var height = -1
}
```

#### IDevice Interface Extension

A parameter is added to the `requestVideoStream` method:

```kotlin
fun requestVideoStream(
    tag: String,
    videoStreamParam: GlassVideoStreamParam = GlassVideoStreamParam(),
    callback: (isSuccess: Boolean) -> Unit
)
```

**Feature Description**:

- Supports specifying a custom resolution when the phone requests camera data from the glasses
- Works with CameraShareConfig in open-sdk to implement end-to-end custom resolution control
- A default value of 0 indicates using the system default configuration, maintaining backward compatibility

**Usage Example**:

```kotlin
// Request a 1280x720@30fps video stream
val extra = GlassVideoStreamParam(width = 1280, height = 720, fps = 30)
deviceService?.requestVideoStream("my_tag", extra) { success ->
    if (success) {
        // Start receiving the video stream
    }
}
```

### Glasses SDK API Changes

#### GlassSdk.kt Enhancements

- **New compatibility methods**: Three compatibility methods are added for the offline command service to ensure backward compatibility between new and old SDK versions:
  - `setOfflineCmdLanguage(language: String): Boolean` - Set the offline command language
  - `setOfflineCmdWords(language: String, voiceActions: List<VoiceAction>): Boolean` - Override offline command words by language
  - `clearOfflineCmdWords(language: String): Boolean` - Clear offline command words for a specified language

These methods use a try-catch mechanism to handle compatibility issues between new and old system-server versions.

#### CameraShareHelper.kt New Features

- **Configurable camera sharing**: New camera sharing methods with configuration support
  - `initSurfaceWithConfig(config: CameraShareConfig, callback: SurfaceCallback)` - Configurable Surface sharing initialization
  - `initNv21ExportWithConfig(enableMix: Boolean, config: CameraShareConfig, callback: Nv21Callback)` - Configurable NV21 export

- **Dynamic configuration change notifications**: New callback methods support runtime configuration changes
  - `onSurfaceShareConfigChanged(width: Int, height: Int, appliedPreviewFps: Int, videoStabilizationEnabled: Boolean)` - Surface sharing configuration change notification
  - `onNv21ExportResolutionChanged(width: Int, height: Int, appliedPreviewFps: Int)` - NV21 export resolution change notification
  - `onNv21ExportRuntimeParamsChanged(appliedPreviewFps: Int, videoStabilizationEnabled: Boolean)` - NV21 export runtime parameter change notification

- **Zoom support**: Adds a zoom level change callback
  - `onZoomLevelChanged(zoomLevel: Int)` - Zoom level change notification (supported in both SurfaceCallback and Nv21Callback)

- **Get supported preview resolutions**: Adds a method to get the list of preview resolutions supported by the Camera API
  - `getSupportedPreviewSizes(): List<Triple<Int, Int, Boolean>>` - Get supported preview resolutions (the camera sensor orientation is rotated 270 degrees, so the width and height are swapped compared with the resolutions passed to initSurfaceWithConfig and initNv21ExportWithConfig)

#### IDeviceService.aidl New APIs

```javascript
interface IDeviceService {

      /**
       * Set capture light
       * @param timestamp Timestamp
        */
     void setCameraLedEnable(boolean enable);


      /**
       * Set network access mode: 0 phone access; 1 glasses access
       * @param timestamp Timestamp
        */
     void setNetworkType(int type);



     int getNetworkType();


    /**
     * Configure app visibility and the display order of third-party apps
     * Used to batch configure the show/hide status of built-in system apps and third-party apps.
     *
     * @param appConfig App configuration information, including:
     *                  - appList: List of built-in system apps to hide
     *                  - thirdApps: Third-party app list, which will be displayed at the front
     * @param callback Set result callback; success=true indicates success, false indicates failure
     *
     */
     void configureAppVisibility(in  GlassAppConfig appConfig, IAppVisibilityListener callback);

    /**
     * Set the time synchronization server address
     * Supports HTTP/HTTPS and NTP protocols, and automatically identifies the protocol type based on the URL
     *
     * @param serverUrl Time server address, supporting the following formats:
     *                  - HTTP/HTTPS protocol: https://time.example.com or http://time.example.com
     *                  - NTP protocol: ntp.example.com or 192.168.1.100
     *                  - null or empty string: Use the default NTP protocol and public NTP servers
     *
     * @note If the URL starts with http:// or https://, HTTP is used to obtain the time
     *       Otherwise, NTP is used (standard UDP protocol, port 123)
     */
    void setTimeServer(String serverUrl);

    /**
     * Get the currently configured time server address
     *
     * @return Currently configured time server address; returns null if not configured
     */
    String getTimeServer();
}
```

#### IMediaServer.aidl New APIs

```java
interface IMediaServer {
  /**
     * Start cross-process Surface sharing (renders only camera frames to the Surface, without NV21 data)
     * @param surface Surface created by the client to receive camera frames
     * @param callback Lifecycle callback
     */
    void startCameraShare(in Surface surface, ICameraSurfaceCallback callback);

    /**
     * Stop cross-process Surface sharing
     * @param callback Previously registered callback
     */
    void stopCameraShare(ICameraSurfaceCallback callback);

    /**
     * Start NV21 data export (share frame data through SharedMemory double buffering)
     * @param callback Callback interface that receives frame data through onNv21FrameAvailable
     * @param enableMix Whether to enable overlay mode (camera + screen overlay),
     *                  false: Export pure camera NV21 data
     *                  true:  Export NV21 data after overlaying camera with screen content
     */
    void startCameraNv21Export(ICameraShareCallback callback, boolean enableMix);

    /**
     * Stop NV21 data export
     * @param callback Previously registered callback
     */
    void stopCameraNv21Export(ICameraShareCallback callback);

    /**
     * Close camera
     * @param callback Callback interface: true indicates the camera was closed successfully, false indicates the camera was not closed
     */
    void closeCamera(ICameraCloseCallback callback);

    /**
     * Start NV21 export (preview resolution, target frame rate, and stabilization can be specified; binary compatibility with the old interface is maintained by client-side call selection).
     * When multiple sessions share the same physical camera, the later configuration preempts the pipeline; existing sessions are synchronized through
     * {@link com.rokid.security.system.server.media.callback.ICameraShareCallback#onNv21ExportResolutionChanged}
     * or {@link com.rokid.security.system.server.media.callback.ICameraShareCallback#onNv21ExportRuntimeParamsChanged}.
     */
    void startCameraNv21ExportWithConfig(ICameraShareCallback callback, boolean enableMix, in CameraShareConfig config);

    /**
     * Start cross-process Surface sharing (configurable version)
     * @param surface Surface created by the client to receive camera frames
     * @param callback Lifecycle callback
     * @param enableMix Whether to enable AR Mix mode (camera + screen overlay)
     * @param config Configuration items, consistent with CameraShareConfig
     */
    void startCameraShareWithConfig(in Surface surface, ICameraSurfaceCallback callback, boolean enableMix, in CameraShareConfig config);

    /**
     * Get the list of preview resolutions supported by the Camera API
     * @return Supported resolution list (camera raw resolution, landscape orientation)
     */
    List<CameraSize> getSupportedPreviewSizes();
}
```

#### ICameraSurfaceCallback.aidl New APIs

```java
/**
 * Lightweight callback for cross-process Surface sharing (lifecycle only, no NV21 data)
 */
interface ICameraSurfaceCallback {

    /**
     * Camera opened
     * @param width Camera width
     * @param height Camera height
     */
    void onCameraOpened(int width, int height);

    /**
     * Camera closed
     */
    void onCameraClosed();

    /**
     * An error occurred
     * @param code Error code
     * @param msg Error message
     */
    void onError(int code, String msg);

    /**
     * Unique callback identifier
     */
    String getCallbackId();

    /**
     * Surface sharing configuration changed (resolution, frame rate, stabilization, etc.)
     * Triggered when configuration is updated through startCameraShareWithConfig to avoid restarting the camera
     * @param width New width
     * @param height New height
     * @param appliedPreviewFps Negotiated frame rate
     * @param videoStabilizationEnabled Whether stabilization is enabled
     */
    void onSurfaceShareConfigChanged(int width, int height, int appliedPreviewFps, boolean videoStabilizationEnabled);

    /**
     * Zoom level changed
     * @param zoomLevel Current zoom level (integer)
     */
    void onZoomLevelChanged(int zoomLevel);
}

```

#### ICameraShareCallback.aidl New APIs

```java
interface ICameraShareCallback {

    /**
     * Camera opened, with resolution and NV21 double-buffered SharedMemory provided
     * @param width Camera width
     * @param height Camera height
     * @param nv21Buffer0 NV21 double buffer 0
     * @param nv21Buffer1 NV21 double buffer 1
     */
    void onCameraOpened(int width, int height, in SharedMemory nv21Buffer0, in SharedMemory nv21Buffer1);

    /**
     * A new NV21 frame has been written to the specified buffer
     * @param bufferIndex Buffer index (0 or 1)
     * @param width Frame width
     * @param height Frame height
     * @param timestamp Frame timestamp (milliseconds)
     */
    void onNv21FrameAvailable(int bufferIndex, int width, int height, long timestamp);

    /**
     * Camera closed
     */
    void onCameraClosed();

    /**
     * An error occurred
     * @param code Error code
     * @param msg Error message
     */
    void onError(int code, String msg);

    /**
     * Unique callback identifier
     */
    String getCallbackId();
    /**
     * A new NV21 frame has been written to the specified buffer (asynchronous version, better performance)
     * @param bufferIndex Buffer index (0 or 1)
     * @param width Frame width
     * @param height Frame height
     * @param timestamp Frame timestamp (milliseconds)
     */
    oneway void onNv21FrameAvailableAsync(int bufferIndex, int width, int height, long timestamp);
    /**
     * The NV21 output size or SharedMemory buffer has switched (because multiple apps preempt the pipeline, or because the session actively changed configuration).
     * The client should close the old mapping and remap using the new SharedMemory. Subsequent {@link #onNv21FrameAvailable} callbacks use the width and height from this callback.
     *
     * @param width NV21 logical width
     * @param height NV21 logical height
     * @param nv21Buffer0 New double buffer 0
     * @param nv21Buffer1 New double buffer 1
     * @param appliedPreviewFps Upper bound of the preview AE target frame-rate range negotiated by the device (representative value)
     */
    void onNv21ExportResolutionChanged(int width, int height, in SharedMemory nv21Buffer0, in SharedMemory nv21Buffer1, int appliedPreviewFps);

    /**
     * The output size is unchanged, but the preview frame rate or stabilization switch changed due to pipeline reconfiguration.
     *
     * @param appliedPreviewFps Representative negotiated preview frame rate (upper bound of the AE range)
     * @param videoStabilizationEnabled Whether video stabilization is currently requested
     */
    void onNv21ExportRuntimeParamsChanged(int appliedPreviewFps, boolean videoStabilizationEnabled);

    /**
     * Zoom level changed
     * @param zoomLevel Current zoom level (integer)
     */
    void onZoomLevelChanged(int zoomLevel);
}
```

## V2.1.7-E(2026-4-7)

- **Phone SDK version**: `com.rokid.security:phone.sdk:2.1.7-E`
- **Glasses SDK version**: `com.rokid.security:glass3.open.sdk:2.1.7-E`
- **Recommended OTA version**: `1.11.e003-20260331-150201` or later

### Phone SDK API Changes

**RTC module removed:** All RTC-related engine parameters and initialization configurations were removed from `EngineParam.kt` and `MobileEngineServerImpl.kt` (under `sdk-base` and `sdk-server`, respectively). In other words, the SDK no longer includes real-time audio/video calling features.

#### Phone SDK: AR Recording API Added

The `getIARMixControl()` method is added to `IWifiP2PClientOperate`, returning an `IARMixControl` object. Integrators can obtain the P2P capability object through `PSecuritySDK.getWifiP2PClientService()`, then call `getIARMixControl()` to get the AR recording control interface and configure AR recording on the phone side (such as enable/disable and parameter adjustment).

> Example 1

```kotlin
private val mIARMixControl by lazy {
    PSecuritySDK.getWifiP2PClientService()?.getIARMixControl()
}

// Set AR mixed recording switch
binding.switchArMix.setOnCheckedChangeListener { _, isChecked ->
    if (GlobalData.btConnectState.value) {
        mIARMixControl?.setARMixEnabled(isChecked) { success ->
            if (!success) {
                L.i(TAG, "setARMixEnabled: Fail")
                getARMixState()
            }
        }
    } else {
        Toast.makeText(this, "Please connect the device over Bluetooth first", Toast.LENGTH_LONG).show()
    }
}

// Get AR mixed recording state
private fun getARMixState() {
    if (GlobalData.btConnectState.value) {
        val activityRef = WeakReference(this)
        mIARMixControl?.getARMixState(
            action = { isOpen ->
                activityRef.get()?.runOnUiThread {
                    L.i(TAG, "getARMixState: $isOpen")
                }
            },
            onFail = { errMsg, code ->
                activityRef.get()?.runOnUiThread {
                    L.i(TAG, "getARMixState: onFail --- errMsg=$errMsg")
                }
            }
        )
    }
}
```

> Example 2

```kotlin
class GlassVideoStreamParam {
    var fps = 20                     // Frame rate
    var bitrate = 5184000            // Video bitrate
    /**
     * Whether to enable AR overlay mode (camera feed + screen content)
     * When true, the video stream obtained through P2P is the real-time image after overlaying camera feed with screen content
     * Default value is false
     */
    var isARMixEnabled = false
}

// Request video stream
val param = GlassVideoStreamParam().apply {
    this.fps = fps
    this.bitrate = bitrate
    this.isARMixEnabled = true
}
PSecuritySDK.getAbsDeviceInfoService()?.requestVideoStream(VIDEO_TAG, videoStreamParam = param) {}
```

### Glasses SDK API Changes

#### IMediaServer Adds the Following Four APIs

```kotlin
public interface IMediaServer extends IInterface {
	/**
     * Start cross-process Surface sharing (renders only camera frames to the Surface, without NV21 data)
     * @param surface Surface created by the client to receive camera frames
     * @param callback Lifecycle callback
     */
    void startCameraShare(in Surface surface, ICameraSurfaceCallback callback);

    /**
     * Stop cross-process Surface sharing
     * @param callback Previously registered callback
     */
    void stopCameraShare(ICameraSurfaceCallback callback);

    /**
     * Start NV21 data export (share frame data through SharedMemory double buffering)
     * @param callback Callback interface that receives frame data through onNv21FrameAvailable
     * @param enableMix Whether to enable overlay mode (camera + screen overlay),
     *                  false: Export pure camera NV21 data
     *                  true:  Export NV21 data after overlaying camera with screen content
     */
    void startCameraNv21Export(ICameraShareCallback callback, boolean enableMix);

    /**
     * Stop NV21 data export
     * @param callback Previously registered callback
     */
    void stopCameraNv21Export(ICameraShareCallback callback);
}
```

> Usage can refer to the `SendMessageActivity` class in the demo.

#### `IMediaServer` Parameter Addition

The `isArRMixEnabled` parameter is added to the `startRecord` method (implemented by adding this field to `RecordConfig`) to control whether AR mixed recording is enabled.

```kotlin
@Keep
@Parcelize
data class RecordConfig(
    val path: String,
    val min: Int,
    val enableAudio: Boolean,
    val width: Int = 1280,
    val height: Int = 720,
    val fps: Int = 20,
    /**
     * Whether AR overlay is enabled for this recording (camera feed + screen content).
     * Default value is false，Old clients keep false when this field is not passed.
     * When applied, the glasses side merges this with the persistent AR screen recording switch on the phone side.
     */
    val isArRMixEnabled: Boolean = false,
) : Parcelable {
    override fun toString(): String {
        return "RecordConfig(path='$path', min=$min, enableAudio=$enableAudio, width=$width, height=$height, fps=$fps, isArRMixEnabled=$isArRMixEnabled)"
    }
}
```

> Usage can refer to the `SdkMediaActivity` class in the demo.

## V2.1.5-E(2026-2-5)

- **Phone SDK version**: `com.rokid.security:phone.sdk:2.1.5-E`
- **Glasses SDK version**: `com.rokid.security:glass3.open.sdk:2.1.5-E`
- **Recommended OTA version**: `1.11.e003-20260131-150201` or later

### Phone SDK API Changes

#### IBTRingClientListener Adds a Method:

```kotlin
interface IBTRingClientListener {

    /***
     * Bluetooth connection status callback
     * @param extra Callback parameter
     */
    fun onConnect(extra: RingExtra)

}
```

### Glasses SDK API Changes

#### IDeviceService Adds a Method:

```kotlin
void setSystemTime(long var1) throws RemoteException;

// Usage example: set the glasses-side system time after SDK initialization succeeds
GlassSdk.getGlassDeviceService()?.setSystemTime(1770014682490)
```

## V2.1.04-E(2026-1-16)

- **Phone SDK version**: `com.rokid.security:phone.sdk:2.1.04-E`
- **Glasses SDK version**: `com.rokid.security:glass3.open.sdk:2.1.04-E`
- **Recommended OTA version**: `1.11.e002-20260116-150201` or later

### New Features

* GB-standard/RTC supports landscape 1080p display
* Added Scan-to-connect Wi-Fi app: [Wi-Fi QR code generator](https://glassar.rokid.com/userguide/rokid-glass/wifi)
* Added glasses-side audio prompts for Bluetooth, app connection, and low battery
* Added phone-side default startup app configuration; after selection, the glasses enter that app by default on boot
* Fixed removing a single offline voice command

### Phone SDK API Changes

#### IDevice Adds Methods:

```kotlin
package com.rokid.security.phone.sdk.api.device

import com.rokid.security.sdk.base.common.GlassVideoStreamParam
import com.rokid.security.sdk.base.common.out.GlassDeviceInfo


interface IDevice {

	/**
     * Request pulling the glasses-side video stream
     * @param tag Video stream request identifier, used to cancel the video stream request
     * @param videoStreamParam  Modify video frame rate
     * @param callback true indicates video stream request success; false indicates failure
     */
    fun requestVideoStream(tag: String,videoStreamParam: GlassVideoStreamParam = GlassVideoStreamParam(),callback: (isSuccess: Boolean) -> Unit)

    /**
     * Stop pulling the glasses-side video stream
     * @param tag Video stream stop identifier, used to cancel stopping the video stream
     * @param callback true indicates video stream stopped successfully; false indicates failure
     */
    fun stopVideoStream(tag: String,callback: (isSuccess: Boolean) -> Unit)
    /**
     * Request pulling the glasses-side audio stream
     * @param tag Audio stream identifier, used to cancel the audio stream request
     * @param callback true indicates audio stream request success; false indicates failure
     */
    fun requestAudioStream(tag: String,callback: (isSuccess: Boolean) -> Unit)
    /**
     * Stop pulling the glasses-side audio stream
     * @param tag Audio stream identifier, used to cancel stopping the audio stream
     * @param callback true indicates audio stream stopped successfully; false indicates failure
     */
    fun stopAudioStream(tag: String,callback: (isSuccess: Boolean) -> Unit)
}
```

#### IFileOperate Adds Methods:

```kotlin
package com.rokid.security.phone.sdk.api.msg.api

import com.rokid.security.phone.sdk.api.msg.listener.FileReceiveListener
import com.rokid.security.phone.sdk.api.msg.listener.FileReceiveV2Listener
import com.rokid.security.phone.sdk.api.msg.listener.IMessageListener
import com.rokid.security.phone.sdk.base.data.callback.IResult
import java.io.File

/**
 * Created by wjm on 2025/5/9
 */
interface IFileOperate {

    /**
     *  Add the local file receiving listener
     *  @param listener
     */
    @Deprecated(
        message = "Deprecated in version V2.1.4; use addFileReceiveV2Listener",
        replaceWith = ReplaceWith("addFileReceiveV2Listener(listener)"),
        level = DeprecationLevel.WARNING
    )
    fun addFileReceiveListener(listener: FileReceiveListener)

    /**
     * Remove the local file receiving listener
     *  @param listener
     */
    @Deprecated(
        message = "Deprecated in version V2.1.4; use removeFileReceiveV2Listener",
        replaceWith = ReplaceWith("removeFileReceiveV2Listener(listener)"),
        level = DeprecationLevel.WARNING
    )
    fun removeFileReceiveListener(listener: FileReceiveListener)

    /**
     *  Add the local file receiving listener
     *  @param listener
     */
    fun addFileReceiveV2Listener(listener: FileReceiveV2Listener)

    /**
     * Remove the local file receiving listener
     *  @param listener
     */
    fun removeFileReceiveV2Listener(listener: FileReceiveV2Listener)

}
```

#### IBluetoothRing Method Change:

```kotlin
package com.rokid.security.phone.sdk.api.bluetooth.ring.api

import android.bluetooth.BluetoothDevice
import com.rokid.security.phone.sdk.api.bluetooth.classic.listener.IClassicBTClientListener

/**
 * Author: zhangshengwei
 * Date: 2025/6/24
 */
interface IBluetoothRing {

    /**
     * Start classic Bluetooth scan
     * @param timeoutMillis Scan timeout setting
     * */
    fun startScan(
        deviceNameFilter:List<String> = listOf("D01"),
        timeoutMillis: Long = 20000L,
    ){}
}
```

### Glasses SDK API Changes

#### ICommonInfoListener Adds a Method:

```kotlin
/**
 * Created by wjm on 2025/9/3
 */
interface ICommonInfoListener {

    /**
     * Configuration information on the glasses side
     */
    fun onConfig(config: String)
}
```

#### PreviewResolution: Video File Resolution During Recording

```kotlin
/**
 * Video file resolution during recording
 */
object PreviewResolution{
    val ResolutionInfo_720P  = ResolutionInfo("720P", 1280, 720) //Preview
    val ResolutionInfo_1080P  = ResolutionInfo("1080P", 1080,1920) //Preview

    // Added landscape camera preview
    val ResolutionInfo_1080P_Land  = ResolutionInfo("1080P_Land", 1920, 1080) //Preview
}
```

## v2.1.2 (2025-12-15)

- **Phone SDK version**: `com.rokid.security:phone.sdk:2.1.2-E`
- **Glasses SDK version**: `com.rokid.security:glass3.open.sdk:2.1.2-E`
- **Recommended OTA version**: `1.10.e001-20251215-150202` or later

### New Features

- Added offline TTS small voice model support
- Added key listener support
- System face recognition algorithm upgraded to NPU acceleration for faster recognition
- Supports dual-system mode
- Developer mode enabled by default

### Phone SDK API Changes

#### IWifiP2PClientOperate Adds Methods:

```kotlin
interface IWifiP2PClientOperate {
    /**
     * Get P2P group information
     * @param action Callback function that returns a WifiP2pGroup object
     */
    fun getGroupInfo(action: (group: WifiP2pGroup?) -> Unit)

    /**
     * Get the P2P connection strategy management instance
     * @return IP2PConnectControl interface instance
     */
    fun getIP2PConnectControl(): IP2PConnectControl
}
```

#### **IAIChat Adds Methods**:

```kotlin
interface IAIChat {
    /**
     * Remove AI chat listener
     */
    fun removeAiChatListener()

    /**
     * Get runtime chat history
     * @return Mutable chat history list
     */
    fun getRunTimeChatList(): MutableList<ChatBean>
}
```

### Glasses SDK API Changes

#### GlassSdk Adds Service Getter Methods

```kotlin
object GlassSdk {
    /**
     * Get image collection service (face/license plate, etc.)
     * @return ICollectService instance
     */
    @JvmStatic
    fun getGlassCollectService(): ICollectService? {
        return getService(ServerType.COLLECT, ICollectService::class.java)
    }

    /**
     * Get offline TTS management service
     * @return IOfflineTtsService instance
     */
    @JvmStatic
    fun getGlassOfflineTtsService(): IOfflineTtsService? {
        return getService(ServerType.SERVICE_OFFLINE_TTS, IOfflineTtsService::class.java)
    }
}
```

#### **New ICollectService Interface**:

```kotlin
/**
 * Image collection service interface
 * Provides collection, transfer, and query capabilities for face, license plate, and other images
 */
interface ICollectService {

    /**
     * Start image collection
     * @param mode Collection mode
     */
    fun startCollect(mode: Int)

    /**
     * Stop image collection
     */
    fun stopCollect()

    /**
     * Set image collection listener
     * @param listener Collection listener instance
     */
    fun setCollectListener(listener: IGlassCollectListener)

    /**
     * Remove image collection listener
     * @param listener Listener instance to remove
     */
    fun removeCollectListener(listener: IGlassCollectListener)

    /**
     * Send face image to a specified target
     * @param identifier Target identifier (such as device ID, user ID, etc.)
     * @param param Collection parameters, including collection configuration information
     * @param callback Send result callback
     */
    fun sendFacePicture(identifier: String, param: CollectParam, callback: ICollectCallback)

    /**
     * Send license plate recognition image to a specified target
     * @param identifier Target identifier
     * @param platNo License plate number
     * @param callback Send result callback
     */
    fun sendLPRPicture(identifier: String, platNo: String, callback: ICollectCallback)

    /**
     * Switch collection mode
     * @param mode New collection mode
     */
    fun changeMode(mode: Int)

    /**
     * Get small face image
     * @param frameId Frame ID
     * @param trackId Track ID
     * @return Small face Bitmap; returns null if retrieval fails
     */
    fun getFaceSmallBitmap(frameId: Long, trackId: Long): Bitmap?

    /**
     * Get small license plate image
     * @param plateNo License plate number
     * @return Small license plate Bitmap; returns null if retrieval fails
     */
    fun getLprSmallBitmap(plateNo: String): Bitmap?
}
```

#### **New IOfflineTtsService Interface**:

```kotlin
interface IOfflineTtsService {
    /**
     * Play speech for the specified text
     * @param ttsMsg Text content to be spoken
     */
    fun playTtsMsg(ttsMsg: String)
}
```

#### **IMediaServer Adds Listener Management Methods**:

```kotlin
interface IMediaServer {
    /**
     * Register media service status listener
     * @param listener Listener instance
     */
    fun setMediaStateLister(listener: IMediaStateLister)

    /**
     * Unregister media service status listener
     * @param listener Listener instance
     */
    fun removeMediaStateLister(listener: IMediaStateLister)
}
```

#### **IOfflineRecServer Adds Face Recognition Result Reporting Method**:

```kotlin
interface IOfflineRecServer {
    /**
     * Report face recognition result
     * @param info Face recognition submission information
     * @param face Face image Bitmap
     */
    fun submitRecognizedFaceInfo(info: RecognizeFaceSubmitInfo, face: Bitmap)
}
```
