Skip to content
On this page

Media3 version 1.9.2

1. Introduction

Integration information:

get_customer_info

2. Require

Prerequisite

  • Operating system: Android 16+

  • 16 KB page alignment support:

    • AGP 8.5.1 or higher: Required for 16 KB uncompressed shared libraries to align on a 16 KB zip-aligned boundary.

    • AGP 8.5 or lower: Switch to compressed shared libraries by adding the following to your app/build.gradle:

      groovy
      android {
        ...
        packagingOptions {
            jniLibs {
              useLegacyPackaging true
            }
        }
      }
  • Add a repositories in the [root_directory]/build.gradle file

    java
    repositories{
          google()
          mavenCentral()
          maven
              {
                  url "https://maven.sigmadrm.com"
              }
          }
      }
  • Add a dependency in the app/build.gradle. The following will add a dependency to the full library:

java
implementation 'androidx.media3:media3-exoplayer:1.9.2'
  // FIXME: If you don't use feature license encrypt, please comment line below
  implementation 'com.sigma.packer:media3-1.9.2:1.0.3'
  • Params that using in this document
PropsTypeDescription
MEDIA_URLStringThe URL to the manifest file
LICENSE_URLStringUrl of the license server
MERCHANT_IDStringIdentify of merchant
APP_IDStringIdentify of application
USER_IDStringUser Identify that provided by merchant system
SESSION_IDStringSession Identify that provided by merchant system

3. Integrate MediaDrmCallback

java
// WidevineMediaDrmCallback.java

import static androidx.media3.exoplayer.drm.DrmUtil.executePost;
import static java.nio.charset.StandardCharsets.UTF_8;

import android.net.Uri;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;

import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import androidx.annotation.NonNull;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.drm.ExoMediaDrm.KeyRequest;
import androidx.media3.exoplayer.drm.ExoMediaDrm.ProvisionRequest;
import androidx.media3.exoplayer.drm.MediaDrmCallback;
import androidx.media3.exoplayer.drm.MediaDrmCallbackException;

import com.google.common.collect.ImmutableMap;
import com.google.common.net.HttpHeaders;
import com.google.common.net.MediaType;
import com.google.common.primitives.Bytes;

// FIXME: If you user license encrypt feature then please uncomment 2 lines below
// import com.sigma.packer.RequestInfo;
// import com.sigma.packer.SigmaDrmPacker;

/**
 * A {@link MediaDrmCallback} that makes requests using {@link DataSource}
 * instances.
 */
@UnstableApi
public final class WidevineMediaDrmCallback implements MediaDrmCallback {
    private final DataSource.Factory dataSourceFactory;
    private final String defaultLicenseUrl;
    private final boolean forceDefaultLicenseUrl;
    private final Map<String, String> keyRequestProperties;

    /**
     * @param defaultLicenseUrl The default license URL. Used for key requests that
     *                          do not specify
     *                          their own license URL.
     * @param dataSourceFactory A factory from which to obtain {@link DataSource}
     *                          instances.
     */
    public WidevineMediaDrmCallback(String defaultLicenseUrl, DataSource.Factory dataSourceFactory) {
        this(defaultLicenseUrl, false, dataSourceFactory);
    }

    /**
     * @param defaultLicenseUrl      The default license URL. Used for key requests
     *                               that do not specify
     *                               their own license URL, or for all key requests
     *                               if {@code forceDefaultLicenseUrl} is
     *                               set to true.
     * @param forceDefaultLicenseUrl Whether to use {@code defaultLicenseUrl} for
     *                               key requests that
     *                               include their own license URL.
     * @param dataSourceFactory      A factory from which to obtain
     *                               {@link DataSource} instances.
     */
    public WidevineMediaDrmCallback(String defaultLicenseUrl, boolean forceDefaultLicenseUrl,
            DataSource.Factory dataSourceFactory) {
        this.dataSourceFactory = dataSourceFactory;
        this.defaultLicenseUrl = defaultLicenseUrl;
        this.forceDefaultLicenseUrl = forceDefaultLicenseUrl;
        this.keyRequestProperties = new HashMap<>();
    }

    /**
     * Sets a header for key requests made by the callback.
     *
     * @param name  The name of the header field.
     * @param value The value of the field.
     */
    public void setKeyRequestProperty(String name, String value) {
        Assertions.checkNotNull(name);
        Assertions.checkNotNull(value);
        synchronized (keyRequestProperties) {
            keyRequestProperties.put(name, value);
        }
    }

    /**
     * Clears a header for key requests made by the callback.
     *
     * @param name The name of the header field.
     */
    public void clearKeyRequestProperty(String name) {
        Assertions.checkNotNull(name);
        synchronized (keyRequestProperties) {
            keyRequestProperties.remove(name);
        }
    }

    /**
     * Clears all headers for key requests made by the callback.
     */
    public void clearAllKeyRequestProperties() {
        synchronized (keyRequestProperties) {
            keyRequestProperties.clear();
        }
    }

    // Wrapping into a RuntimeException is recommended by the JSONException docs:
    // https://developer.android.com/reference/org/json/JSONException
    @SuppressWarnings("ThrowSpecificExceptions")
    @NonNull
    @Override
    public Response executeProvisionRequest(@NonNull UUID uuid, ProvisionRequest request)
            throws MediaDrmCallbackException {
        byte[] httpBody = Bytes.concat(
                "{\"signedRequest\":\"".getBytes(UTF_8), request.getData(), "\"}".getBytes(UTF_8));
        return executePost(
                dataSourceFactory.createDataSource(),
                request.getDefaultUrl(),
                httpBody,
                ImmutableMap.of(
                        HttpHeaders.CONTENT_TYPE,
                        MediaType.JSON_UTF_8.toString(),
                        HttpHeaders.CONTENT_LENGTH,
                        String.valueOf(httpBody.length)));
    }

    @NonNull
    @Override
    public Response executeKeyRequest(@NonNull UUID uuid, @NonNull KeyRequest request)
            throws MediaDrmCallbackException {
        try {
            String url = request.getLicenseServerUrl();
            if (forceDefaultLicenseUrl || TextUtils.isEmpty(url)) {
                url = defaultLicenseUrl;
            }
            Map<String, String> requestProperties = new HashMap<>();
            // Add standard request properties for supported schemes.
            String contentType = "application/octet-stream";
            requestProperties.put("Content-Type", contentType);
            requestProperties.put("custom-data", getCustomData(request));

            // Add additional request properties.
            synchronized (keyRequestProperties) {
                requestProperties.putAll(keyRequestProperties);
            }
            Response response = executePost(
                    dataSourceFactory.createDataSource(),
                    url,
                    request.getData(),
                    requestProperties);
            JSONObject jsonObject = new JSONObject(new String(response.data));
            String licenseEncrypted = jsonObject.getString("license");
            return new Response(Base64.decode(licenseEncrypted, Base64.DEFAULT));
        } catch (MediaDrmCallbackException e) {
            throw e;
        } catch (Exception e) {
            throw new MediaDrmCallbackException(
                    new DataSpec.Builder().setUri(Uri.EMPTY).build(),
                    Uri.EMPTY,
                    /* responseHeaders= */ ImmutableMap.of(),
                    /* bytesLoaded= */ 0,
                    /* cause= */ new IllegalStateException("Error while parsing response", e));
        }
    }

    private String getCustomData(KeyRequest keyRequest) throws Exception {
        JSONObject customData = new JSONObject();
        customData.put("merchantId", MERCHANT_ID);
        customData.put("appId", APP_ID);
        customData.put("userId", USER_ID);
        customData.put("sessionId", SESSION_ID);

//      FIXME: If you user license encrypt feature then please uncomment 3 lines below
//      RequestInfo requestInfo = SigmaDrmPacker.requestInfo(keyRequest.getData());
//      customData.put("reqId", requestInfo.requestId);
//      customData.put("deviceInfo", requestInfo.deviceInfo);

        String customHeader = Base64.encodeToString(customData.toString().getBytes(), Base64.NO_WRAP);
        Log.e("SIGMA", "Custom data " + customHeader);
        return customHeader;
    }
}

4. Initialize Player

java
// PlayerActivity.java

  private void initializePlayer() {
    DrmSessionManager drmSessionManager;
    if (Util.SDK_INT >= 18) {
      UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid("widevine"));
      MediaDrmCallback drmCallback = createMediaDrmCallback(drmLicenseUrl, null);
      drmSessionManager =
              new DefaultDrmSessionManager.Builder()
                      .setMultiSession(true)
                      .setUuidAndExoMediaDrmProvider(drmSchemeUuid, SigmaMediaDrm.DEFAULT_PROVIDER)
                      .build(drmCallback);
    } else {
      drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED;
    }

    MediaItem mediaItem = MediaItem.fromUri(Uri.parse(videoPath));
    MediaSource.Factory mediaSourceFactory =
            new DefaultMediaSourceFactory(getApplicationContext())
                    .setDrmSessionManagerProvider(mi -> drmSessionManager);
    MediaSource mediaSource = mediaSourceFactory.createMediaSource(mediaItem);

    trackSelector = new DefaultTrackSelector(/* context= */ this);
    DefaultTrackSelector.Parameters trackSelectionParameters =
            new DefaultTrackSelector.ParametersBuilder(/* context= */ this)
                    .setAllowVideoMixedMimeTypeAdaptiveness(true)
                    .setAllowVideoNonSeamlessAdaptiveness(true)
                    .build();
    player = new ExoPlayer.Builder(getApplicationContext())
            .setTrackSelector(trackSelector)
            .build();
    player.setTrackSelectionParameters(trackSelectionParameters);
    player.setMediaSource(mediaSource);
    player.prepare();
    player.play();
    playerView.setPlayer(player);
    player.addAnalyticsListener(new EventLogger(trackSelector));
  }

  private WidevineMediaDrmCallback createMediaDrmCallback(String licenseUrl, String[] keyRequestPropertiesArray) {
    HttpDataSource.Factory licenseDataSourceFactory =
            ((ExoplayerApplication) getApplication()).buildHttpDataSourceFactory();
    WidevineMediaDrmCallback drmCallback =
            new WidevineMediaDrmCallback(licenseUrl, licenseDataSourceFactory);
    if (keyRequestPropertiesArray != null) {
      for (int i = 0; i < keyRequestPropertiesArray.length - 1; i += 2) {
        drmCallback.setKeyRequestProperty(keyRequestPropertiesArray[i],
                keyRequestPropertiesArray[i + 1]);
      }
    }
    return drmCallback;
  }

5. Proguard Configuration

If you use the License Encrypt feature, add the following Proguard rules:

java
-keep class com.sigma.packer.RequestInfo { *; }
-keep class com.sigma.packer.SigmaDrmPacker { *; }
-keep class com.sigma.packer.SigmaMediaDrm { *; }

-keepclasseswithmembers class com.sigma.packer.RequestInfo$* { *; }
-keepclasseswithmembers class com.sigma.packer.SigmaDrmPacker$* { *; }
-keepclasseswithmembers class com.sigma.packer.SigmaMediaDrm$* { *; }

6. Sample code

Sample source code