面向智能体的最佳实践

更新时间:
复制为 MD 格式

本文通过两个端到端 Demo 链路,演示 Agent 基于 llms.txt 索引接入阿里云视频点播(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

阿里云控制台

开通视频点播服务

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上传上传时指定转码模板组自动转码、通过事件回调感知进度转码完成后获取播放地址的完整服务端链路。

提交给AgentPrompt

请基于llms.txt文档(https://ice-document-materials.oss-cn-shanghai.aliyuncs.com/vod/llms/llms.txt),用 Python 实现视频点播批量上传、自动转码以及获取播放地址的完整流程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
  "上传时自动转码"     → 媒体处理/工作流.md

Common mistakes 命中的约束:
  #1 不用主账号 AK  #3 不轮询状态  #5 URL 需编码  #6 QPS 退避

阶段 2:按需加载详细文档

文档

获取目的

媒体上传/URL拉取上传.md

UploadMediaByURL 参数和限制

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

回调机制、事件类型

API参考/获取音视频播放地址.md

GetPlayInfo 参数和返回结构

API参考/初始化_2.md

Python SDK 初始化方式

媒体处理/工作流.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                      │
│  ✓ "自动转码"       → 媒体处理/工作流.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索引按场景定位文档并生成代码。

提交给AgentPrompt

请基于客户端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 初始化、媒体处理工作流)。

  • 明确指出不要使用主账号 AccessKey,应使用 RAM 子账号

如果三项都通过,则Coding Agent 已具备运行本文 Demo 的能力。