In this tutorial, you use an AI Coding Agent to build an end-to-end ApsaraVideo VOD workflow. The server-side demo batch-uploads videos by URL, automatically transcodes them with a specified transcoding template group, and retrieves playback URLs through event callbacks. The client-side demo provides an Android app with single video playback and an interactive live streaming viewer powered by AUI Kits.
Prerequisites
Server-side demo
An AccessKey pair for a RAM user, stored as environment variables. Do not use a root account AccessKey.
Python with the Alibaba Cloud SDK for VOD (
aliyunsdkcoreandaliyunsdkvod) installed.A publicly accessible HTTP endpoint to receive event notification callbacks.
VOD region set to
cn-shanghai(URL-based uploads are only supported in the China (Shanghai) region).
Client-side demo
Android Studio with
compileSdk 34andminSdk 21.An AliPlayer license file (
license.crt) placed inapp/src/main/assets/.AUI Kits modules (
AUILiveRoomCore,AUILiveRoomViewer,AUIInteraction) imported into the project.An AppServer endpoint that issues viewer tokens for the interactive live streaming viewer.
Server-side demo: Batch upload, transcode, and playback
The following code submits a batch upload job with a transcoding template group. Specifying TemplateGroupId during upload triggers automatic transcoding, eliminating the need for a separate API call.
request = UploadMediaByURLRequest()
request.set_UploadURLs(','.join([urllib.parse.quote(url, safe='') for url in urls])) # URLs must be encoded!
request.set_UploadMetadatas(json.dumps([{'SourceURL': urllib.parse.quote(u, safe=''), 'Title': f'Video{i}'} for i, u in enumerate(urls)]))
request.set_TemplateGroupId('your_template_group_id') # Key: Specify this to auto-transcode after upload
response = json.loads(client.do_action_with_exception(request))
# Returns a list of JobIds. Track progress via event notifications.HTTP callback service (key logic): Returns HTTP 200 immediately to prevent server-side retries (constraint S-7).
class CallbackHandler(BaseHTTPRequestHandler):
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
self.send_response(200) # Must return 200 immediately; retry is triggered after 5s timeout
self.end_headers()
if body['EventType'] == 'UploadByURLComplete' and body['Status'] == 'success':
print(f"Upload complete: VideoId={body['VideoId']}, waiting for transcoding...")
elif body['EventType'] == 'TranscodeComplete' and body['Status'] == 'success':
# All transcodings complete -> Get playback URLs
play_info = get_play_info(body['VideoId'])
for stream in play_info['PlayInfoList']['PlayInfo']:
print(f" [{stream['Definition']}] {stream['PlayURL']}")Get playback URL (key logic):
request = GetPlayInfoRequest()
request.set_VideoId(video_id)
request.set_Formats('mp4,m3u8')
request.set_AuthTimeout(3600) # URL is valid for 1 hour
response = json.loads(client.do_action_with_exception(request))
# response['PlayInfoList']['PlayInfo'] → playback URLs for each definitionExecution results
Submitting batch upload:
$ python vod_batch_upload.py
[1/2] Initializing VOD client... ✓ (region: cn-shanghai)
[2/2] Submitting batch upload job (3 videos)... ✓
RequestId: 25818875-5F78-4AF6-D7393642CA58****
JobId: ad90a501b1b9**** ← sample1.mp4
JobId: bd81b612c2c8**** ← sample2.mp4
JobId: ce72c723d3d9**** ← sample3.mp4
Subsequent steps are triggered automatically by event notifications:
UploadByURLComplete → Auto Transcode → TranscodeComplete → GetPlayInfoCallback service receives events:
$ python callback_server.py
Callback service started: http://0.0.0.0:8080 | Waiting for callbacks...
[UploadByURLComplete] VideoId=93ab850b**** | Status=success
→ Upload complete, waiting for auto-transcoding...
[StreamTranscodeComplete] VideoId=93ab850b**** | Status=success
→ LD definition transcoding complete (ready for playback)
[TranscodeComplete] VideoId=93ab850b**** | Status=success
→ All transcodings complete, getting playback URLs:
[LD] https://vod.example.com/****/sample1-ld.mp4?auth_key=****
[SD] https://vod.example.com/****/sample1-sd.mp4?auth_key=****
[HD] https://vod.example.com/****/sample1-hd.mp4?auth_key=****Agent document retrieval
Prompt: "Batch upload + transcode + playback"
│
▼
┌── llms.txt (Quick start matching + Common mistakes constraints) ──┐
│ ✓ "Batch URL upload" → media-upload/url-based-upload.md │
│ ✓ "Event notification" → event-notification/event-notification_2.md │
│ ✓ "Playback URL" → api-reference/get-playback-url.md │
│ ✓ "Python init" → api-reference/initialization_2.md │
│ ✓ "Auto transcode" → media-processing/workflow.md │
│ ✗ #1 Don't use root AK ✗ #3 Don't poll ✗ #5 URL encode ✗ #6 QPS backoff │
└────────────────────────┬─────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
url-based-upload.md event-notification_2.md get-playback-url.md
- UploadMediaByURL - Event type list - Request/response params
- URL must be encoded - HTTP callback format - Definition enums
- Region limit - Timeout & retry policy - QPS=360/s
- Max 20 URLs - 5 sec timeout - Status=Normal
│ │ │
▼ ▼ ▼
└────► initialization_2.md (SDK client constructor) ◄────┘
│
▼
workflow.md (Auto-transcode with TemplateGroupId)
│
▼
Generate complete runnable codeKey design decisions
The following table lists key design decisions for this demo, with explicit constraint IDs traced back to the structured constraint reference.
Decision point | Agent's choice | Basis in documentation |
Upload method | UploadMediaByURL | The documentation specifies this API for batch uploading from a list of public URLs. |
Transcoding trigger | Specify TemplateGroupId during upload | The documentation states that specifying |
Status tracking | HTTP event callback | Constraint S-3 (MUST): Do not poll. |
Timing for getting playback URL | After TranscodeComplete | The documentation states that |
AccessKey management | Environment variables | Constraint S-1, S-2 (MUST): Do not hardcode. |
URL handling |
| Constraint S-5 (MUST): Must be encoded. |
Region | cn-shanghai | The documentation states that URL-based uploads are only supported in the China (Shanghai) region. |
Fault tolerance | Exponential backoff retry | Constraint S-6 (MUST): Backoff for throttling. |
Client-side demo: Android player and live viewer
This section builds a runnable Android demo on top of the server-side workflow. The demo plays an MP4 file with minimal code and integrates an interactive live streaming viewer with AUI Kits.
Prompt submitted to the agent:
Based on the client-side llms index (in directories like `client-side-player/`,
`client-side-upload/`, `client-side-short-video/`), implement a demo app for Android.
Specific requirements:
1. Select the minimum required dependency package to play the playback URL returned
by the server-side demo's GetPlayInfo response.
2. In the same app, add another entry point to enter an interactive live streaming
viewer using AUI Kits (with stream pulling, live comments (danmaku), likes,
and gifts).
3. Make the package name, license, and AppServer domain configurable to avoid
hardcoding.Agent execution process
Phase 1: Read the client-side llms index and match scenarios
Quick start matching results:
"Start VOD playback on Android" → client-side-player/player-atomic-android.md
"How to choose packages/dependencies" → client-side-player/player-overview.md
"How to integrate the license" → client-side-player/player-common-license.md
"Interactive live streaming viewer" → client-side-player/player-kits-aui.md
"Error codes 4400/4013" → client-side-player/player-common-error-codes.md
Common mistakes matched:
✗ Initializing the license after creating the player → Must be done in Application.onCreate (C-1)
✗ Calling setSurface before the Surface is ready → Must wait for surfaceCreated (C-2)
✗ Using the full-featured package by default → Choose the minimal package for the scenario (C-3)
✗ Hardcoding the AppServer domain → Abstract to BuildConfig / gradle.properties (C-4)
✗ Calling PrivateService.init multiple times → Initialize only once in the Application class (C-5)Phase 2: Load detailed documentation on demand
Document | Purpose |
client-side-player/player-overview.md | Package selection decision (BasicLive / UGC / InteractiveLive / Standard) |
client-side-player/player-atomic-android.md | Gradle dependencies, Manifest permissions, obfuscation, and minimal playback code |
client-side-player/player-common-license.md | License integration timing and troubleshooting for errors 4400/4013 |
client-side-player/player-kits-aui.md | AUI Kits client-side modules and AppServer collaboration model |
client-side-player/player-common-error-codes.md | Quick reference for playback start failures |
Phase 3: Extract key decisions
Package selection:
Playback only → AliVCSDK_BasicLive
Playback + short video → AliVCSDK_UGC
Includes co-hosting/interactive live streaming → AliVCSDK_InteractiveLive ← This demo chooses this one
Full-featured → AliVCSDK_Standard / Premium
License:
PrivateService.initService(ctx, licenseFile, licenseKey)
Must be completed before the first call to createAliPlayer
AUI Kits:
AppServer + client-side; AppServer issues Identity Token, Live Room Token, and IMS Token.
Client: Three modules - AUILiveRoomCore, AUILiveRoomViewer, and AUIInteraction.Set up project dependencies
In the project-level build.gradle file:
allprojects {
repositories {
google()
mavenCentral()
maven { url "https://maven.aliyun.com/nexus/content/repositories/releases" }
}
}In the app-level build.gradle file:
android {
compileSdk 34
defaultConfig {
applicationId "com.example.aliplayer.sample"
minSdk 21
targetSdk 34
ndk { abiFilters "armeabi-v7a", "arm64-v8a" }
buildConfigField "String", "APP_SERVER_HOST",
"\"${project.findProperty('APP_SERVER_HOST') ?: 'https://your-appserver.example.com'}\""
buildConfigField "String", "APP_SERVER_API_PREFIX", "\"/api/v1/live/\""
buildConfigField "String", "LICENSE_KEY",
"\"${project.findProperty('LICENSE_KEY') ?: ''}\""
}
buildFeatures { buildConfig true }
}
dependencies {
implementation 'com.aliyun.aio:AliVCSDK_InteractiveLive:6.0.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.google.code.gson:gson:2.10.1'
}Configure permissions and obfuscation
In the AndroidManifest.xml file:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />In the proguard-rules.pro file:
-keep class com.alivc.**{*;}
-keep class com.aliyun.**{*;}
-keep class com.cicada.**{*;}
-dontwarn com.alivc.**
-dontwarn com.aliyun.**
-dontwarn com.cicada.**Initialize the license
Create the SampleApp.java file:
public class SampleApp extends Application {
@Override
public void onCreate() {
super.onCreate();
// Place license.crt in app/src/main/assets/
PrivateService.initService(getApplicationContext(),
"file:///android_asset/license.crt",
BuildConfig.LICENSE_KEY);
}
}Register SampleApp in the AndroidManifest.xml file:
<application
android:name=".SampleApp"
android:label="@string/app_name"
android:theme="@style/Theme.MaterialComponents.DayNight">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".SimplePlayerActivity" />
</application>Build the single video playback UI
Create the activity_player.xml layout file:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000">
<SurfaceView
android:id="@+id/surfaceView"
android:layout_width="match_parent"
android:layout_height="220dp"
android:layout_gravity="center" />
</FrameLayout>Create the SimplePlayerActivity.java file:
public class SimplePlayerActivity extends AppCompatActivity {
public static final String EXTRA_PLAY_URL = "play_url";
private AliPlayer aliPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_player);
SurfaceView surfaceView = findViewById(R.id.surfaceView);
aliPlayer = AliPlayerFactory.createAliPlayer(getApplicationContext());
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override public void surfaceCreated(SurfaceHolder h) {
aliPlayer.setSurface(h.getSurface());
}
@Override public void surfaceChanged(SurfaceHolder h, int f, int w, int hh) {
aliPlayer.surfaceChanged();
}
@Override public void surfaceDestroyed(SurfaceHolder h) {
aliPlayer.setSurface(null);
}
});
aliPlayer.setOnPreparedListener(() -> aliPlayer.start());
aliPlayer.setOnErrorListener(err ->
Log.e("AliPlayer", "code=" + err.getCode() + " msg=" + err.getMsg()));
UrlSource source = new UrlSource();
// Source: The playback URL (with auth_key) from the server-side demo's GetPlayInfo response
String playUrl = getIntent().getStringExtra(EXTRA_PLAY_URL);
source.setUri(playUrl != null ? playUrl : "https://your.cdn/test.mp4");
aliPlayer.setDataSource(source);
aliPlayer.setAutoPlay(true);
aliPlayer.prepare();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (aliPlayer != null) {
aliPlayer.stop();
aliPlayer.release();
}
}
}If the prepared event and the first frame event appear in the logs and the SurfaceView renders the image correctly, the underlying link is clear.
Integrate the AUI Kits live viewer
Import AUI Kits modules
In the settings.gradle file:
include ':app',
':AUILiveRoomCore',
':AUILiveRoomViewer',
':AUIInteraction'In the app-level build.gradle file, add the following dependencies:
implementation project(':AUILiveRoomCore')
implementation project(':AUILiveRoomViewer')
implementation project(':AUIInteraction')AppServer collaboration model
The client never holds sensitive credentials directly. Instead, the AppServer issues them after authentication. The following diagram shows the complete workflow for entering a live room:
[Android App] [AppServer]
│ │
│── GET /api/v1/live/viewer/token?roomId=xxx ─►│
│ ├── Authenticates and issues:
│ │ userId / nick / pullUrl / imsToken
│◄──────── 200 OK + ViewerTokenInfo ───────────┤
│
│── LiveRoomViewerActivity.start(params) ──►(Inside AUI Kits)
│ │
│ (AliPlayer for stream pulling + IMS for live comments)The Android app sends a
GET /api/v1/live/viewer/token?roomId=xxxrequest to the AppServer with the login state.The AppServer verifies the login status and issues the
userId,nick,pullUrl, andimsToken.After the app receives
ViewerTokenInfo, it callsLiveRoomViewerActivity.start(params)to enter the live room.Internally, AUI Kits uses AliPlayer for stream pulling and connects to the Interactive Messaging Service (IMS) for live comments (danmaku), like, and gift features.
AppServerClient (OkHttp implementation)
Create the AppServerClient.java and ViewerTokenInfo.java files:
public class AppServerClient {
public interface Callback {
void onSuccess(ViewerTokenInfo info);
void onFail(String msg);
}
private static final OkHttpClient client = new OkHttpClient();
public static void fetchViewerToken(String roomId, Callback cb) {
HttpUrl url = HttpUrl.parse(
BuildConfig.APP_SERVER_HOST + BuildConfig.APP_SERVER_API_PREFIX + "viewer/token")
.newBuilder()
.addQueryParameter("roomId", roomId)
.build();
Request req = new Request.Builder()
.url(url)
.header("Authorization", "Bearer " + LoginManager.getToken())
.build();
client.newCall(req).enqueue(new okhttp3.Callback() {
@Override public void onFailure(Call call, IOException e) {
cb.onFail(e.getMessage());
}
@Override public void onResponse(Call call, Response resp) throws IOException {
if (!resp.isSuccessful() || resp.body() == null) {
cb.onFail("HTTP " + resp.code());
return;
}
ViewerTokenInfo info = new Gson().fromJson(
resp.body().string(), ViewerTokenInfo.class);
cb.onSuccess(info);
}
});
}
}
public class ViewerTokenInfo {
public String roomId;
public String userId;
public String nick;
public String pullUrl; // Any of rtmp, flv, m3u8, or artc
public String imsToken; // Interactive Messaging Service (IMS) Token
}Combine the demo entries
Create the activity_main.xml layout file:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button android:id="@+id/btnSimple"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Simple Playback" />
<Button android:id="@+id/btnLiveRoom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Interactive Live Viewer" />
</LinearLayout>Create the MainActivity.java file:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btnSimple).setOnClickListener(v -> {
Intent intent = new Intent(this, SimplePlayerActivity.class);
// In a real application, pass the playback URL from the server-side demo's GetPlayInfo response here
intent.putExtra(SimplePlayerActivity.EXTRA_PLAY_URL,
"https://vod.example.com/****/sample1-hd.mp4?auth_key=****");
startActivity(intent);
});
findViewById(R.id.btnLiveRoom).setOnClickListener(v -> enterRoom("demo_room_001"));
}
private void enterRoom(String roomId) {
AppServerClient.fetchViewerToken(roomId, new AppServerClient.Callback() {
@Override public void onSuccess(ViewerTokenInfo info) {
LiveRoomEnterParams params = new LiveRoomEnterParams.Builder()
.roomId(info.roomId)
.userId(info.userId)
.userNick(info.nick)
.pullUrl(info.pullUrl)
.imsToken(info.imsToken)
.build();
LiveRoomViewerActivity.start(MainActivity.this, params);
}
@Override public void onFail(String msg) {
Toast.makeText(MainActivity.this, "Failed to enter room: " + msg, Toast.LENGTH_LONG).show();
}
});
}
}Verification checklist
After running the demo, verify the following UI behavior and log output:
Click Simple Playback: The
SimplePlayerActivitystarts, and the video plays smoothly in theSurfaceViewwithout a black screen.Click Interactive Live Streaming Viewer: The AUI Kits
LiveRoomViewerActivityis launched. AliPlayer displays the host's video feed. The live comments (danmaku), like, and gift panels in the bottom right corner are functional.Key logcat logs: The simultaneous appearance of
AliPlayer: prepared,AliPlayer: first frame, andIMS: connectedindicates that both stream pulling and interaction are ready.
Agent document retrieval
Prompt: "Android playback + enter interactive live room"
│
▼
┌── Client-side llms.txt (Quick start + Common mistakes) ────────┐
│ ✓ "Android playback" → player-atomic-android.md │
│ ✓ "How to choose package" → player-overview.md │
│ ✓ "License" → player-common-license.md │
│ ✓ "AUI Kits interactive" → player-kits-aui.md │
│ ✓ "Error codes 4400/4013" → player-common-error-codes.md │
│ ✗ License must be initialized before player creation │
│ ✗ Do not call setSurface before Surface is ready │
│ ✗ Do not use the full-featured package by default │
│ ✗ Do not hardcode the AppServer domain │
│ ✗ Call PrivateService.init only once in Application │
└────────────────────────┬────────────────────────────────────┘
│
┌─────────────┬────────┴─────────┬─────────────┐
▼ ▼ ▼ ▼
player- player-atomic- player-common- player-kits-aui.md
overview.md android.md license.md - AppServer collab
- Package matrix - gradle deps - PrivateService - Three token types
- Scenario map - Manifest perms - 4400/4013 - Module breakdown
- SDK size - Playback code - Package/BundleID - 4-step client integration
└─────────────┴────────┬─────────┴─────────────┘
│
▼
player-common-error-codes.md
(Guidance for 4400/4013/4034/4036)
│
▼
Generate complete runnable Android DemoKey design decisions
Decision point | Agent's choice | Basis in documentation |
SDK bundle | AliVCSDK_InteractiveLive | The selection matrix in |
License timing | Application.onCreate | Constraint C-1 (MUST): Must be called before |
Surface binding |
| Constraint C-2 (MUST): |
AppServer domain | BuildConfig + gradle.properties | Constraint C-4 (MUST): Do not hardcode. |
Server-side integration | Reuse the playback URL from the server-side demo's GetPlayInfo response | This ensures end-to-end consistency between the server-side and client-side demos. |
Interactive live viewer | AUI Kits three modules (Core / Viewer / Interaction) |
|
Error handling |
|
|
Pre-launch checklist
Dimension | Check item |
License | Test and production licenses have been applied for and configured in their respective BuildConfigs. |
Package size |
|
Playback start | Listeners for 4400/4013/4034/4036 are implemented for monitoring. |
Live room | Automatic renewal for expired IMS Tokens is implemented. AppServer has HTTPS and CORS enabled. |
Privacy | Permissions for audio recording, network access, and media reading are all declared in the privacy policy. |
Canary release | First, release the new version to 5% of users. Monitor the first-frame time and failure rates for 48 hours before a full rollout. |
Monitoring | Enable Alibaba Cloud's player quality monitoring service (QoS) and single-point tracing. |
Final directory structure
AliPlayerSampleApp/
├── app/
│ ├── src/main/
│ │ ├── assets/license.crt
│ │ ├── java/com/example/aliplayer/sample/
│ │ │ ├── SampleApp.java
│ │ │ ├── MainActivity.java
│ │ │ ├── SimplePlayerActivity.java ← Simple Playback
│ │ │ ├── AppServerClient.java ← Interactive Live Streaming
│ │ │ ├── ViewerTokenInfo.java
│ │ │ └── LoginManager.java
│ │ └── AndroidManifest.xml
│ └── build.gradle
├── AUILiveRoomCore/ ← Interactive Live Streaming
├── AUILiveRoomViewer/ ← Interactive Live Streaming
├── AUIInteraction/ ← Interactive Live Streaming
├── gradle.properties (APP_SERVER_HOST / LICENSE_KEY)
├── settings.gradle
└── build.gradleThe server-side and client-side demos form a complete, closed-loop workflow. Using the llms.txt index, the Agent covers video uploading, automatic transcoding, playback URL retrieval, basic video playback, and an interactive live streaming viewer with AUI Kits.
Appendix: Agent selection and self-check
This section is for developers who have not installed a Coding Agent.
When to use a Coding Agent
Use case | Is it needed? |
You want the AI to read | Yes |
You only want to read this document to understand the architecture, then copy and manually modify the code. | No. A web browser is sufficient. |
You want the AI to directly add, delete, and modify files in your local repository and run commands to validate the changes. | Yes |
Recommended options
Category | Name | Type | Description |
Recommended | Qoder / Cursor / Claude Code | Desktop IDE or CLI | Natively supports long-document retrieval using |
Other | - | - | Any Coding Agent that can access URLs, write local files, and call shell commands. |
Use the installation links from the official product website. Avoid using third-party images.
Minimum self-check
Send the following prompt to your Coding Agent to verify its capabilities:
Please read https://ice-document-materials.oss-cn-shanghai.aliyuncs.com/vod/llms/llms.txt,
and tell me:
1. How many top-level modules are in this index? What are they?
2. Which sub-documents are involved in the "batch upload + automatic transcoding + get audio/video playback URL" workflow? Please list their relative paths.
3. In the "Common mistakes to avoid" section, what are the notes related to "AccessKey"?Expected answers:
Correctly lists the top-level modules from
llms.txt.Lists sub-documents largely consistent with the on-demand loading table in the server-side demo (media upload, event notification, get audio/video playback URL, Python SDK initialization, media processing workflow).
Clearly states: Do not use an Alibaba Cloud account AccessKey. Use a RAM user instead.
An agent that passes all three checks is ready for the demos above.