To test this feature and view the example code, please see the Android SDK 5 Example Code Quick Start guide.

The NexGuard library forensic watermarking tool QuickMark embeds a unique, invisible serial number onto video/audio content. It functions in two modes:

  • Push mode: The application retrieves the QuickMark blob (for example, through a gateway) and pushes it to the SDK.
  • Pull mode: The application configures the QuickMark client SDK to access a QuickMark server. The SDK handles the communication with the server to retrieve the blob.

The minimum Android API for using the watermarking feature is 21 (Lollipop 5.0).
Watermark requires Android System Webview Version 42 or higher to be installed on the device.
Having any UI element rendered over the OTVVideoView may prevent the watermark from working properly.

Prerequisites

A working watermark requires the following parameters to work correctly:

  • A context to call Watermark constructor.
  • A valid OTVVideoView to bind the watermark to.
  • A valid customer/system generated token of type String.
  • A valid URL of type String.
  • A valid secret key of type String.
  • The last optional value is to set the ApiKey which is also of type String. This parameter is not mandatory but may be required depending on the server deployment - it is used to restrict access to the server to authorized users.

If these values are available, a working QuickMarkView can be created.
The core class of the watermarking function is Watermark. Its purpose is to take the above parameters passed by the user to create a new object of the QuickMarkView class and bind it to the provided OTVVideoView before starting the watermark playback. The class is also responsible for handling error message events and starting and stopping the QuickMarkView whenever video is played or paused.

Currently, cases that trigger an error are:

  • Null tenant value
  • Null token value
  • Null server URL value
  • No server response
  • A URL shorter than four characters

The following errors do not get thrown (errors are silently ignored):

  • Invalid URL string
  • Incorrect token value

Dependencies

The QuickMark library is embedded within the SDK .aar library but brings a dependency on the OkHttp library. Include the following in the dependencies block in your Gradle build script:

dependencies {
	…
	implementation 'com.squareup.okhttp3:okhttp:4.9.0'
	…
}
GROOVY

Example code

Attaching a watermark is quite simple if you have the prerequisite parameters above. Since OTVVideoView is required to bind and unbind the QuickMarkView, NAGRA recommends you tie the watermark lifecycle to the OTVVideoView lifecycle. Brief examples of implementing the watermark feature in each mode are shown below.

…
import nagra.otv.sdk.OTVVideoView;
import nagra.otv.sdk.watermark.Watermark;
import nagra.otv.sdk.watermark.WatermarkErrorListener;
import nagra.otv.sdk.watermark.WatermarkMessageListener;

public class MainActivity extends Activity {
  …
  private static final String STREAM_URI = "https://example.stream.address";

  private static final String WATERMARK_URL = "https://example.watermark.address";
  private static final String WATERMARK_TOKEN = "WATERMARK_TOKEN";
  private static final String WATERMARK_API_KEY = "API_KEY";
  private static final String WATERMARK_SECRET_KEY = "SECRET_KEY";

  private FrameLayout mFrame = null;
  private OTVVideoView mOTVVideoView = null;

  private Watermark mWatermark;
  private WatermarkErrorListener mWatermarkErrorListener = (watermarkErrorId, s) ->
    Log.d(TAG, "Watermark error: " + s);
  private WatermarkMessageListener mWatermarkMessageListener = s ->
    Log.d(TAG, "Watermark message: " + s);

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    …
    mFrame = findViewById(R.id.frame);

    if (mOTVVideoView == null) {
      mOTVVideoView = new OTVVideoView(this);
      mOTVVideoView.setVideoPath(STREAM_URI);
      mFrame.addView(mOTVVideoView);
    }

    mWatermark = Watermark.createInPullMode(this, WATERMARK_URL, WATERMARK_TOKEN, WATERMARK_API_KEY, WATERMARK_SECRET_KEY);
    if (mOTVVideoView != null && mWatermark != null) {
      mWatermark.addErrorListener(mWatermarkErrorListener);
      mWatermark.addMessageListener(mWatermarkMessageListener);
      mWatermark.bind(mOTVVideoView);
    }
  }
  …
  //Match the watermark lifecycle with the video view lifecycle.
  @Override
  public void onDestroy() {
    super.onDestroy();
    if (mOTVVideoView != null && mWatermark != null) {
      mWatermark.removeErrorListener(mWatermarkErrorListener);
      mWatermark.removeMessageListener(mWatermarkMessageListener);
      mWatermark.unbind(mOTVVideoView);
    }
  }
}
JAVA

All mWatermark configuration must be set before the binding call.

…
import nagra.otv.sdk.watermark.WatermarkErrorId;
import nagra.otv.sdk.OTVLog;
import nagra.otv.sdk.OTVSDK;
import nagra.otv.sdk.OTVVideoView;
import nagra.otv.sdk.watermark.Watermark;
import nagra.otv.sdk.watermark.WatermarkErrorListener;
import nagra.otv.sdk.watermark.WatermarkMessageListener;

public class MainActivity extends Activity {
  …
  private static final String SECRET_KEY = "SECRET_KEY";
  
  private OTVVideoView mOTVVideoView = null;
  private Watermark mWatermark;

  private WatermarkMessageListener mWMMessageListener = message ->
      Log.i(TAG, "Watermark: " + message);

  private WatermarkErrorListener mWMErrorListener = (errorId, message) ->
      Log.e(TAG, "Watermark error: " + errorId + "," + message);

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    …

    //Create and config Watermark in push mode
    mWatermark = Watermark.createInPushMode(this, SECRET_KEY);
    if (mWatermark != null) {
      mWatermark.addMessageListener(mWMMessageListener);
      mWatermark.addErrorListener(mWMErrorListener);
      mWatermark.bind(mOTVVideoView);
    } else {
      Toast.makeText(getBaseContext(), "Create watermark failed.", Toast.LENGTH_LONG).show();
    }

    …
  }

  // You may want to update the watermark periodically, or upon some external event
  private void updateBlob(byte[] blobData) {
    boolean bSuccess = mWatermark.setBlob(blobData, true);
    if (!bSuccess) {
      //The Application can stop playback according to own rules.
    } else {
      // The update is successful
    }
  }

  @Override
  protected void onDestroy() {
    super.onDestroy();
    if (mOTVVideoView != null) {
      mWatermark.unbind(mOTVVideoView);
    }
  }
}

JAVA