全部產品
Search
文件中心

ApsaraVideo VOD:面向智能體的最佳實務

更新時間:Jun 05, 2026

本文通過兩個端到端 Demo 鏈路,示範 Agent 基於 llms.txt 索引接入阿里雲ApsaraVideo for VOD(VOD)服務的可複用最佳實務。

說明

請確保已閱讀面向智能體的入門指南並理解 llms.txt 的基本結構。如果首次接觸,請先閱讀該指南。

說明

如果尚未安裝任何Coding Agent,請先閱讀本文末尾附錄:Coding Agent選型與自檢完成準備。

Agent 使用 llms.txt 的完整工作流程

本模組定義 Agent 與開發人員的通用分工與工作流程,是後續兩個 Demo 實踐的方法論基礎。Demo實踐 中的關鍵設計決策表為該方法論在具體情境下的落地,可對照閱讀。

角色分工:開發人員 vs Agent

人與 Agent 的分工邊界如下表。開發人員負責需求表達、環境準備與結果驗收,Agent 負責文檔檢索、方案設計與代碼產生。

階段

開發人員

Agent

需求發起

用自然語言描述需求

-

文檔檢索

-

讀取 llms.txt 索引,匹配情境,定位詳細文檔

文檔理解

-

擷取詳細文檔,提取 API 參數、樣本、注意事項

方案設計

確認或調整方案

基於文檔提出實現方案,說明技術選型理由

代碼產生

-

產生可運行代碼,含錯誤處理

環境配置

配置 AccessKey、安裝依賴、控制台設定

提供命令和配置指引

執行驗證

運行代碼,確認結果

解釋輸出,排查問題

Agent 內部工作流程

Agent 在收到需求後,內部按以下四個階段處理。每個階段的產出會作為下一階段的輸入。

階段 1:讀取 llms.txt 索引

一次性載入約 250 行索引檔案,完成以下三件事:

  • 解析 Quick start 章節,建立情境文檔路徑映射。

  • 提取 Common mistakes to avoid,作為後續代碼產生的硬性約束。

  • 理解文檔拓撲結構(一級模組、子模組)。

階段 2:情境匹配與文檔定位

  • 用需求關鍵詞匹配 Quick start 的情境標題。

  • 命中情境:直接取對應文檔路徑。

  • 未命中情境:掃描模組索引,找出最相關的 1~3 個子文檔。

  • 構造 URL: BASE_URL + url_encode(相對路徑)。

階段 3:擷取詳細文檔並提取實現資訊

從子文檔中提取以下要素,作為代碼產生的輸入:API 名稱、請求/返回參數、程式碼範例、QPS 限制、錯誤碼、地區限制、注意事項。

階段 4:代碼產生

在產生代碼前對照 Common mistakes 清單逐條自檢:

  • 使用了主帳號 AccessKey?則改為 RAM 子帳號。

  • 寫了輪詢狀態?則改為事件通知。

  • URL 未編碼?則調用 url_encode

  • 每次請求重建憑證?則複用 Credential 並 Refresh。

並為最終代碼補充 QPS 退避與非同步處理邏輯。

URL 構造規則

索引檔案以相對路徑引用子文檔,Agent 在拉取子文檔前需把相對路徑拼成絕對 URL:

基礎 URL:https://ice-document-materials.oss-cn-shanghai.aliyuncs.com/vod/llms/
文檔 URL = 基礎 URL + URL 編碼(相對路徑,去掉前置 "./")

樣本:
  llms.txt 中:[服務端上傳](./媒體上傳/服務端上傳.md)
  完整 URL:基礎 URL + 媒體上傳/服務端上傳.md

端到端實戰Demo

本模組給出兩個端到端 Demo,覆蓋服務端與用戶端兩側,二者通過 GetPlayInfo 返回的 PlayURL 形成完整閉環。

運行前置條件(人工操作)

步驟

位置

操作

1

阿里雲控制台

開通ApsaraVideo for VOD服務

2

RAM 控制台

建立子帳號 + 授予 AliyunVODFullAccess + 建立 AccessKey

3

點播控制台

配置轉碼模板組(記錄 TemplateGroupId)

4

點播控制台

配置 HTTP 回調地址(組態管理 > 回調設定

5

本地終端

pip install aliyun-python-sdk-core aliyun-python-sdk-vod

6

本地終端

export ALIBABA_CLOUD_ACCESS_KEY_ID=xxxSECRET=xxx

服務端 Demo:批量上傳 + 自動轉碼 + 擷取播放地址

本 Demo 示範 Agent 如何基於 llms.txt 索引完成批量 URL上傳上傳時指定轉碼模板組自動轉碼、通過事件回調感知進度轉碼完成後擷取播放地址的完整服務端鏈路。

提交給Agent的Prompt

請基於llms.txt文檔(https://ice-document-materials.oss-cn-shanghai.aliyuncs.com/vod/llms/llms.txt),用 Python 實現ApsaraVideo for VOD批量上傳、自動轉碼以及擷取播放地址的完整流程Demo。
具體需求為:
1.將一批視頻檔案(通過公網 URL 列表提供)批量上傳到阿里雲點播。
2.上傳完成後自動觸發轉碼(使用指定的轉碼模板組)。
3.轉碼完成後擷取各清晰度的播放地址。
4.需要一個HTTP回調服務接收事件通知。

Agent的執行過程

階段 1:讀取 llms.txt 完成情境匹配

Agent 在索引中匹配到的情境與命中的約束如下:

Quick start 關鍵詞 → 命中的文檔路徑:
  "URL列表批量上傳"  → 媒體上傳/URL拉取上傳.md
  "接收非同步事件通知"  → 事件通知/事件通知_2.md
  "擷取播放地址給前端" → API參考/擷取音視頻播放地址.md
  "Python SDK 初始化" → API參考/初始化_2.md
  "上傳時自動轉碼"     → ApsaraVideo for Media Processing/工作流程.md

Common mistakes 命中的約束:
  #1 不用主帳號 AK  #3 不輪詢狀態  #5 URL 需編碼  #6 QPS 退避

階段 2:按需載入詳細文檔

文檔

擷取目的

媒體上傳/URL拉取上傳.md

UploadMediaByURL 參數和限制

事件通知/事件通知_2.md

回調機制、事件類型

API參考/擷取音視頻播放地址.md

GetPlayInfo 參數和返回結構

API參考/初始化_2.md

Python SDK 初始化方式

ApsaraVideo for Media Processing/工作流程.md

上傳時指定模板組自動轉碼機制

階段 3:提取關鍵資訊

批量上傳:UploadMediaByURL
  - 非同步執行,僅華東 2(上海),單次最多 20 個 URL
  - URL 必須 URLEncode
  - 可指定 TemplateGroupId → 上傳完成後自動轉碼

播放地址:GetPlayInfo
  - 前提:視頻 Status=Normal(轉碼完成後)
  - 返回:PlayInfoList 含各清晰度 PlayURL

事件通知:HTTP POST 回調
  - UploadByURLComplete    → 上傳完成
  - StreamTranscodeComplete → 單清晰度轉碼完成(最早可播放)
  - TranscodeComplete       → 全部轉碼完成
  - 回調逾時 5 秒,最多重試 3 次

階段 4:產生代碼

Agent 基於以上資訊產生核心代碼。

Agent輸出的核心代碼

批量上傳(關鍵邏輯):從環境變數讀取憑證,並對所有 URL 強製做編碼:

# 初始化 - AK從環境變數讀取,不寫入程式碼(Common mistakes #1)
client = AcsClient(region_id='cn-shanghai', credential=AccessKeyCredential(
    os.environ['ALIBABA_CLOUD_ACCESS_KEY_ID'],
    os.environ['ALIBABA_CLOUD_ACCESS_KEY_SECRET']
))

# 批量URL上傳 + 指定轉碼模板組(上傳後自動轉碼)
request = UploadMediaByURLRequest()
request.set_UploadURLs(','.join([urllib.parse.quote(url, safe='') for url in urls]))  # URL必須編碼!
request.set_UploadMetadatas(json.dumps([{'SourceURL': urllib.parse.quote(u, safe=''), 'Title': f'視頻{i}'} for i, u in enumerate(urls)]))
request.set_TemplateGroupId('your_template_group_id')  # 關鍵:指定後上傳完自動轉碼
response = json.loads(client.do_action_with_exception(request))
# 返回 JobId 列表,後續通過事件通知感知進度

HTTP 回調服務(關鍵邏輯):立即返回 200,避免服務端觸發重試:

class CallbackHandler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers['Content-Length'])))
        self.send_response(200)  # 必須立即返回200,超5秒會重試
        self.end_headers()

        if body['EventType'] == 'UploadByURLComplete' and body['Status'] == 'success':
            print(f"上傳完成: VideoId={body['VideoId']},等待轉碼...")

        elif body['EventType'] == 'TranscodeComplete' and body['Status'] == 'success':
            # 全部轉碼完成 → 擷取播放地址
            play_info = get_play_info(body['VideoId'])
            for stream in play_info['PlayInfoList']['PlayInfo']:
                print(f"  [{stream['Definition']}] {stream['PlayURL']}")

擷取播放地址(關鍵邏輯)

request = GetPlayInfoRequest()
request.set_VideoId(video_id)
request.set_Formats('mp4,m3u8')
request.set_AuthTimeout(3600)  # 地址有效期間 1 小時
response = json.loads(client.do_action_with_exception(request))
# response['PlayInfoList']['PlayInfo'] → 各清晰度的 PlayURL

執行結果

提交批量上傳

$ python vod_batch_upload.py

[1/2] 初始化VOD用戶端... ✓ (region: cn-shanghai)
[2/2] 提交批量上傳任務 (3個視頻)... ✓
  RequestId: 25818875-5F78-4AF6-D7393642CA58****
  JobId: ad90a501b1b9****  ← sample1.mp4
  JobId: bd81b612c2c8****  ← sample2.mp4
  JobId: ce72c723d3d9****  ← sample3.mp4

後續流程通過事件通知自動觸發:
  UploadByURLComplete → 自動轉碼 → TranscodeComplete → GetPlayInfo

回調服務接收事件

$ python callback_server.py
回調服務啟動: http://0.0.0.0:8080 | 等待回調...

[UploadByURLComplete] VideoId=93ab850b**** | Status=success
  → 上傳完成,等待自動轉碼...

[StreamTranscodeComplete] VideoId=93ab850b**** | Status=success
  → LD清晰度轉碼完成(已可播放)

[TranscodeComplete] VideoId=93ab850b**** | Status=success
  → 全部轉碼完成,擷取播放地址:
    [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文檔檢索可視化

Prompt: "批量上傳+轉碼+播放"
  │
  ▼
┌── llms.txt (Quick start 匹配 + Common mistakes 約束) ────────┐
│  ✓ "URL 批量上傳"  → 媒體上傳/URL拉取上傳.md                  │
│  ✓ "事件通知"       → 事件通知/事件通知_2.md                  │
│  ✓ "播放地址"       → API參考/擷取音視頻播放地址.md           │
│  ✓ "Python 初始化" → API參考/初始化_2.md                      │
│  ✓ "自動轉碼"       → ApsaraVideo for Media Processing/工作流程.md                      │
│  ✗ #1 不用主帳號  ✗ #3 不輪詢  ✗ #5 URL編碼  ✗ #6 QPS退避     │
└────────────────────────┬─────────────────────────────────────┘
                         │
   ┌─────────────────────┼─────────────────────┐
   ▼                     ▼                     ▼
URL拉取上傳.md       事件通知_2.md       擷取音視頻播放地址.md
- UploadMediaByURL  - 事件類型列表       - 請求/返回參數
- URL必須編碼        - HTTP回調格式       - 清晰度枚舉
- 地區限制           - 逾時&重試策略      - QPS=360/s
- 最多20個URL        - 5秒逾時           - Status=Normal
   │                     │                     │
   ▼                     ▼                     ▼
   └────► 初始化_2.md(SDK 用戶端構造) ◄────┘
                         │
                         ▼
                  工作流程.md(TemplateGroupId 自動轉碼)
                         │
                         ▼
                產生完整可運行代碼

關鍵設計決策(Agent 依據文檔做出)

下表為本Demo的具體決策追溯,與角色分工對應:角色分工說明誰做什麼,本表說明針對本情境,為什麼這樣選。

決策點

Agent 的選擇

文檔依據

上傳方式

UploadMediaByURL

需求是 URL 列表,文檔明確此 API 適用公網 URL 情境

轉碼觸發

上傳時指定 TemplateGroupId

文檔:設定後上傳完自動轉碼,無需額外 API 呼叫

狀態感知

HTTP 事件回調

Common Mistakes 第 3 條:不要輪詢

播放地址時機

TranscodeComplete 後

文檔:Status=Normal 才能 GetPlayInfo

AccessKey 管理

環境變數

Common Mistakes 第 1 條:不寫入程式碼

URL 處理

quote(url, safe='')

Common Mistakes 第 5 條:必須編碼

地區

cn-shanghai

URL 拉取僅支援華東2(上海)

容錯

指數退避重試

Common Mistakes 第 6 條:Throttling 退避

用戶端Demo:Android播放器與互動直播觀眾端

在打通服務端鏈路的基礎上,本模組以Android平台為例提供用戶端可運行 Demo:涵蓋“20行代碼起播 MP4”“基於AUI Kits接入互動直播觀眾端”。同時示範了Agent如何利用用戶端llms.txt索引按情境定位文檔並產生代碼。

提交給Agent的Prompt

請基於用戶端llms索引(端側播放器/、端側上傳/、端側短視頻/ 等目錄),用 Android 實現一個 Demo App。
具體需求為:
1.選取最小依賴包,播放服務端Demo GetPlayInfo 返回的播放地址。
2.在同一個 App 中再加一個入口,使用 AUI Kits 進入互動直播觀眾端(拉流播放 + 彈幕 + 點贊 + 禮物)。
3.包名、License、AppServer 網域名稱均做成可配置,避免寫入程式碼。

Agent的執行過程

階段 1:讀取用戶端 llms 索引,情境匹配

Quick start 匹配結果:
  "Android 起播 VOD"   → 端側播放器/player-atomic-android.md
  "包/依賴怎麼選"       → 端側播放器/player-overview.md
  "License 怎麼接"     → 端側播放器/player-common-license.md
  "互動直播觀眾端"      → 端側播放器/player-kits-aui.md
  "錯誤碼 4400/4013"   → 端側播放器/player-common-error-codes.md

Common mistakes 命中的約束:
  ✗ License 在播放器建立之後才 init      → 必須在 Application.onCreate
  ✗ Surface 還沒 ready 就 setSurface     → 必須等 surfaceCreated
  ✗ 直接拿全功能包                        → 按情境選最小包
  ✗ AppServer 網域名稱寫入程式碼                  → 抽到 BuildConfig / gradle.properties
  ✗ 同一直播間多次 PrivateService.init   → 僅在 SampleApp 裡 init 一次

階段 2:按需載入詳細文檔

文檔

擷取目的

端側播放器/player-overview.md

選包決策(BasicLive / UGC / InteractiveLive / Standard)

端側播放器/player-atomic-android.md

gradle 依賴、Manifest 許可權、混淆、最小起播代碼

端側播放器/player-common-license.md

License 接入時機與 4400/4013 錯誤排查

端側播放器/player-kits-aui.md

AUI Kits 用戶端模組、AppServer 協作模型

端側播放器/player-common-error-codes.md

起播失敗錯誤碼速查

階段 3:提取關鍵決策

選包:
  僅播放          → AliVCSDK_BasicLive
  播放 + 短視頻    → AliVCSDK_UGC
  含連麥/互動直播  → AliVCSDK_InteractiveLive   ← 本 Demo 選它
  全功能          → AliVCSDK_Standard / Premium

License:
  PrivateService.initService(ctx, licenseFile, licenseKey)
  必須在第一次 createAliPlayer 之前完成

AUI Kits:
  AppServer + 用戶端雙側;AppServer 頒發 身份Token / 直播間Token / IMS Token
  用戶端:AUILiveRoomCore + AUILiveRoomViewer + AUIInteraction 三個 module

執行結果

播放器快速整合

工程依賴

在專案級build.gradle檔案中:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url "https://maven.aliyun.com/nexus/content/repositories/releases" }
    }
}

在應用級build.gradle檔案中:

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'
}

許可權與混淆

AndroidManifest.xml檔案中:

<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" />

proguard-rules.pro檔案中:

-keep class com.alivc.**{*;}
-keep class com.aliyun.**{*;}
-keep class com.cicada.**{*;}
-dontwarn com.alivc.**
-dontwarn com.aliyun.**
-dontwarn com.cicada.**

License初始化

public class SampleApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // license.crt 放在 app/src/main/assets/
        PrivateService.initService(getApplicationContext(),
                "file:///android_asset/license.crt",
                BuildConfig.LICENSE_KEY);
    }
}

AndroidManifest.xml檔案中註冊:

<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>
單視頻起播介面

產生activity_player.xml檔案:

<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>

產生SimplePlayerActivity.java檔案:

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();
        // 來源:2.1 節 GetPlayInfo 返回的 PlayURL(已帶 auth_key)
        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();
        }
    }
}

在日誌中看到 prepared 與首幀事件,SurfaceView正常出圖,即代表底層鏈路通暢。

AUI Kits互動直播觀眾端

引入AUI Kits模組

settings.gradle檔案中:

include ':app',
        ':AUILiveRoomCore',
        ':AUILiveRoomViewer',
        ':AUIInteraction'

在應用級build.gradle檔案中

implementation project(':AUILiveRoomCore')
implementation project(':AUILiveRoomViewer')
implementation project(':AUIInteraction')

AppServer 協作模型

用戶端不直接持有 IMS Token 等敏感憑證,由 AppServer 鑒權後下發。一次完整的進入直播間請求流程如下:

[Android App]                                [AppServer]
   │                                              │
   │── GET /api/v1/live/viewer/token?roomId=xxx ─►│
   │                                              ├── 鑒權 + 頒發:
   │                                              │     userId / nick / pullUrl / imsToken
   │◄──────── 200 OK + ViewerTokenInfo ───────────┤
   │
   │── LiveRoomViewerActivity.start(params) ──►(AUI Kits 內部)
   │                                              │
   │   (AliPlayer 拉流 + IMS 彈幕)                 │
  1. Android App 攜帶登入態向 AppServer 發起 GET /api/v1/live/viewer/token?roomId=xxx

  2. AppServer 校正登入態後,頒發 userId / nick / pullUrl / imsToken

  3. App 收到 ViewerTokenInfo 後,調用 LiveRoomViewerActivity.start(params) 進入直播間。

  4. AUI Kits 內部用 AliPlayer 拉流,並通過 IMS 接入彈幕、點贊、禮物。

AppServerClient(OkHttp 實現)

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;   // rtmp/flv/m3u8/artc 任一
    public String imsToken;  // 互動訊息 SDK Token
}
兩個Demo入口合一

產生activity_main.xml檔案:

<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="單視頻起播" />

    <Button android:id="@+id/btnLiveRoom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="互動直播觀眾端" />
</LinearLayout>

產生MainActivity.java檔案:

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);
            // 實際使用時把 2.1 節 GetPlayInfo 的 PlayURL 透傳過來
            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, "進入失敗:" + msg, Toast.LENGTH_LONG).show();
            }
        });
    }
}

驗證清單

完整 Demo 跑通後預期看到的介面與日誌:

  • 點擊單視頻起播:進入 SimplePlayerActivity,黑色背景上的 220dp SurfaceView 正常出畫,畫面流暢、無黑屏。

  • 點擊互動直播觀眾端:AUI Kits 的 LiveRoomViewerActivity 被拉起;AliPlayer 顯示主播畫面;右下方彈幕、點贊、禮物面板均可正常使用。

  • logcat 關鍵日誌:AliPlayer: preparedAliPlayer: first frameIMS: connected 三條同時出現,代表拉流互動兩側均就緒。

Agent 文檔檢索可視化

Prompt: "Android 起播 + 進入互動直播間"
  │
  ▼
┌── 用戶端llms.txt(Quick start + Common mistakes) ───────────┐
│  ✓ "Android 起播"        → player-atomic-android.md         │
│  ✓ "包怎麼選"             → player-overview.md               │
│  ✓ "License"             → player-common-license.md          │
│  ✓ "AUI Kits 互動直播"    → player-kits-aui.md               │
│  ✓ "錯誤碼 4400/4013"     → player-common-error-codes.md     │
│  ✗ License 必須在播放器建立前 init                            │
│  ✗ Surface 未 ready 不可 setSurface                          │
│  ✗ 不要直接拿全功能包                                          │
│  ✗ AppServer 網域名稱不要寫入程式碼                                    │
│  ✗ PrivateService.init 僅在 Application 內一次               │
└────────────────────────┬────────────────────────────────────┘
                         │
  ┌─────────────┬────────┴─────────┬─────────────┐
  ▼             ▼                  ▼             ▼
player-      player-atomic-   player-common-  player-kits-aui.md
overview.md  android.md       license.md      - AppServer 協作
- 選包矩陣   - gradle 依賴     - PrivateService - 三類 Token
- 情境對應   - Manifest 許可權   - 4400/4013     - 模組拆分
- SDK 體積   - 起播代碼        - 包名/BundleID  - 用戶端 4 步整合
  └─────────────┴────────┬─────────┴─────────────┘
                         │
                         ▼
              player-common-error-codes.md
              (4400/4013/4034/4036 處理指引)
                         │
                         ▼
              產生完整可運行 Android Demo

關鍵設計決策(Agent依據文檔做出)

決策點

Agent 的選擇

文檔依據

SDK 組合包

AliVCSDK_InteractiveLive

player-overview.md 選包矩陣:含連麥/互動直播

License 時機

Application.onCreate

player-common-license.md:必須早於 createAliPlayer

Surface 綁定

surfaceCreated回調內 setSurface

player-atomic-android.md 標準模板

AppServer 網域名稱

BuildConfig + gradle.properties

Common Mistake:不寫入程式碼

與服務端聯動

複用服務端Demo GetPlayInfo 返回的PlayURL

端到端一致性

互動直播觀眾端

AUI Kits 三模組(Core / Viewer / Interaction)

player-kits-aui.md:用戶端整合主線

錯誤處理

setOnErrorListener 通用回調打點;4400/4013/4034/4036 留作業務側分支擴充

player-common-error-codes.md

上線前 Checklist(Android)

維度

檢查項

License

測試與生產 License 已分別申請並填入對應 BuildConfig

包體

abiFilters 限制為 armv7 + arm64;按情境選擇最小 SDK 組合包

起播

監聽 4400/4013/4034/4036 並打點

直播間

IMS Token 到期自動續簽;AppServer 已開啟 HTTPS 與 CORS

隱私

錄音、網路、媒體讀取許可權均於隱私協議明示

灰階

新版本先 5% 灰階,48 小時觀察首幀與失敗率再放量

監控

啟用阿里雲播放品質服務(QoS)與單點追查

最終目錄結構

AliPlayerSampleApp/
├── app/
│   ├── src/main/
│   │   ├── assets/license.crt
│   │   ├── java/com/example/aliplayer/sample/
│   │   │   ├── SampleApp.java
│   │   │   ├── MainActivity.java
│   │   │   ├── SimplePlayerActivity.java         ← 單視頻起播
│   │   │   ├── AppServerClient.java              ← 互動直播
│   │   │   ├── ViewerTokenInfo.java
│   │   │   └── LoginManager.java
│   │   └── AndroidManifest.xml
│   └── build.gradle
├── AUILiveRoomCore/                               ← 互動直播
├── AUILiveRoomViewer/                             ← 互動直播
├── AUIInteraction/                                ← 互動直播
├── gradle.properties (APP_SERVER_HOST / LICENSE_KEY)
├── settings.gradle
└── build.gradle

至此,服務端Demo用戶端Demo 構成完整閉環:涵蓋上傳、自動轉碼、擷取播放地址、單視頻起播及AUI Kits 互動直播觀眾端,全流程均由 Agent 基於 llms.txt 索引按需檢索文檔產生。

附錄:Coding Agent 選型與自檢

本附錄面向尚未使用或安裝 Coding Agent的開發人員。

何時需要Coding Agent

使用情境

是否需要

想讓 AI 直接讀 llms.txt、按需要展開子文檔、產生可運行專案工程

需要

僅想看本文檔瞭解架構、複製代碼後人工修改

不需要,瀏覽器即可

想讓 AI 在本地倉庫裡直接增刪改檔案、運行命令進行驗證

需要

推薦選項

分類

名稱

形態

說明

推薦

Qoder / Cursor / Claude Code

案頭 IDE 或 CLI

原生支援 llms.txt 類長文檔檢索,對國內雲廠商 SDK 適配較好;本文 Demo 基於 Qoder 驗證。

其他

-

-

任何能連網讀取 URL、能在本地寫檔案、能調用 Shell 的 Coding Agent 均可。

說明

安裝連結以產品官網為準,避免使用第三方鏡像。

安裝後的最小自檢

將以下Prompt發送給安裝好的Coding Agent,對照預期答案驗證能力:

請幫我讀取 https://ice-document-materials.oss-cn-shanghai.aliyuncs.com/vod/llms/llms.txt,
告訴我:
1.這份索引一共有幾個一級模組?分別是什嗎?
2."批量上傳 + 自動轉碼 + 擷取播放地址"這條鏈路涉及哪幾個子文檔?請列出相對路徑。
3.Common mistakes to avoid 中和 "AccessKey" 相關的有哪幾條?

預期答案

  • 正確列出llms.txt 的一級模組清單。

  • 給出與服務端Demo中按需載入詳細文檔表大致一致的子文檔清單(媒體上傳、事件通知、擷取音視頻播放地址、Python SDK 初始化、ApsaraVideo for Media Processing工作流程)。

  • 明確指出不要使用主帳號 AccessKey,應使用 RAM 子帳號

如果三項都通過,則Coding Agent 已具備運行本文 Demo 的能力。