抖音开放平台Logo
开发者文档
控制台

互动工具开发者指南(抖音 App + 直播伴侣)

收藏
我的收藏

更新日志

日期
更新内容
2024.08.13
    文档初版

名词解释

名词
名词解释
截图(demo)
直播互动工具
「直播互动工具」是「直播间互动面板」中一种互动玩法,旨在激发主播与直播间观众互动。它分为两部分:「互动插件玩法面板」和「互动插件玩法卡片」。以下简称「插件」、「玩法面板」、「玩法卡片」。
直播间互动面板
主播互动工具的集合页,里面有互动工具列表(包含互动插件)。
互动插件详情页
从「直播间互动面板」点击某一个互动插件,会跳转到「互动插件详情页」,主要展示互动插件详细信息,若涉及到付费,需要购买支付才能使用。
互动插件玩法面板
(开发者开发)
从「互动插件详情页」点击打开按钮,会跳转到「互动插件玩法面板」,玩法面板是个半屏小程序页,主要用于「配置玩法规则」和「启动/关闭玩法卡片」。
互动插件玩法卡片
(开发者开发)
玩法面板点击开启按钮,会打开玩法卡片,主要用于激发主播与直播间观众互动。
抖音开放平台
(开发者使用)
提供小程序、直播小玩法、直播互动工具等多业务载体,为开发者提供丰富的能力和解决方案
抖音开发者工具
(开发者使用)
抖音开发者工具是面向抖音开放平台开发者推出的桌面端集成开发环境。开发者工具支持开放平台应用(包含小程序、小游戏等)编辑、调试、预览、上传等基本功能。

客户端下载

抖音开发者工具

请下载 4.3.0 及之后版本:下载地址

抖音 App

    安卓:
app_update_v31.4.0_9f5ec5c-with-plugins_channel_7zip_aligned-signed.apk
    iOS:
    TestFlight

直播伴侣

Tips:需要在 Windows 电脑打开

开发 Demo

demo.zip

整体流程

创建插件

    1.打开抖音开放平台,进入控制台页面,定位到「直播小玩法」Tab 下面(未登录时需要注册/登录)
    2.点击「创建直播小玩法」按钮
    3.选择「互动插件」类型,点击下一步
    4.填写互动插件名称,勾选阅读协议按钮,然后点击「创建」按钮
    5.创建成功后会跳转到控制台,可以查看到刚才创建的互动插件,点击进入插件详情页面
    6.申请开放数据能力
    7.申请平台基础能力,exe类型插件仅在直播伴侣中运行;小程序类型插件在抖音 App 和直播伴侣中均可运行
    8.配置开发产物类型,需要注意,一旦审核完或者上线后,产物配置就不允许再变更了!开发者要提前选择好开发产物类型。
    9.完善基础信息
以上插件创建流程全部走完,可以进入开发阶段。

开发插件

开发过程中可参考 API 文档调试文档
注意:插件真实运行依赖抖音直播间环境。
插件分为两部分:「玩法面板」和「玩法卡片」。
    玩法面板是个半屏小程序页,主要用于「配置玩法规则」和「启动/关闭玩法卡片」。
    玩法卡片是个 Canvas 画布,主要用于「接收直播间开放数据」,当数据到达「玩法面板」所配置的指标时,激发主播与直播间观众互动。
开发前首先要下载 抖音开发者工具(IDE)
下载完成,打开登录后,选择「小玩法」下面的「直播互动插件」,新建项目,填写项目信息,使用空白模板创建。
项目创建完成后,IDE 的模拟器中可以看到「玩法面板」和「玩法卡片」两块区域。
假设互动插件小程序,目录结构如下,其中 live-card 目录用于玩法卡片开发,pages 目录用于玩法面板开发:
├── app.js ├── app.json ├── app.ttss ├── project.config.json ├── pages │ └── index ├── live-card │ ├── card.js
首先需要在app.json文件的 liveCards 字段声明放置玩法卡片代码 JS/TS 文件路径。
{ "liveCards": ["live-card/card"] }

玩法面板开发

玩法面板(后文简写为 Page)实际上就是一个半屏的小程序页面,开发范式和小程序规范一致,但支持 JS API 能力和线上小程序相比会存在删减,且路由能力只支持 redirectTo,即所有路由页面都在同一个小程序面板中承接。
以下是一个标准的玩法面板,由三部分组成:
    顶部 Header 栏:互动插件小程序框架自带,开发者只需要配置 pages 下 index.json 文件即可
{ "usingComponents": {}, "navigationBarTitleText": "实时歌词", "navigationBarSubTitleText": "智能识别当前曲目,自动展示歌词" }
    中间配置区域:用于配置玩法,由开发者自行实现
    底部按钮区域:用于启动/关闭玩法,由开发者自行实现。启动/关闭玩法的本质是通过tt.createLiveCardtt.exitMiniProgram 来打开和关闭玩法卡片。由于卡片是个 Canvas 画布,放置在直播间内,开发者需要先感知卡片展示的最大宽高,可以通过 tt.getLiveRoomCardInfo 来获取。
// 获取卡片最大宽高 tt.getLiveRoomCardInfo({ success(res) { console.log('调用成功:', res.liveCardMaxWidth, res.liveCardMaxHeight); // 启动玩法 tt.createLiveCard({ url: '/live-card/game', width: res.liveCardMaxWidth, // 卡片宽度,可以修改,建议设置一个小于最大宽度的值 height: res.liveCardMaxHeight, // 卡片高度,可以修改,建议设置一个小于最大高度的值 success: async ({ cardContext, errMsg }) => { console.info('Card created!', cardContext, errMsg); }, fail: ({ errMsg }) => { console.error('Create card error: ', errMsg); }, }) }, fail(res) { console.log('调用失败:', res.errMsg); } }) // 关闭玩法 tt.exitMiniProgram({ isFullExit: true, // true:完全关闭,false:隐藏玩法面板 success: (res) => { console.log('完全退出小程序成功') }, fail: (res) => { console.log('完全退出小程序失败') }, });

玩法卡片开发

注:一个小程序内仅支持同时打开一个玩法卡片,确保基础库为 3.41.0 及以上版本。
玩法卡片(后文简写为 Card)必须在 Page 中通过 tt.createLiveCard 进行创建,创建过程中会加载玩法卡片文件 live-card/card.js 中用 Card 构造器 注册的玩法卡片,生成一个 Card 实例,并且触发它的 created 生命周期钩子。
这个 Card 实例将支持 show、hide 等框架提供的能力,同时也支持开发者在 Card 实例上自定义一些能力。这个实例除了在 Card 内能通过 this 获取,也会在 tt.createLiveCard 成功回调返回给 Page。

卡片内容绘制

在 Card 中,目前仅支持 created 生命周期,将在创建 Card 之后执行该逻辑,开发者可以在里面做首次绘制。
Card({ created: function(options) { // 在创建 card 时做一些绘制 } })
另外 Card 内还支持以下事件处理函数,具体参考:
方法名
接口类型
说明
同步
获取当前 canvas 实例
异步
展示当前玩法卡片
异步
隐藏当前玩法卡片
异步
更新玩法卡片的宽、高
一个例子:
let canvas, ctx; Card({ onMessage(openData) { console.log('--openData', openData); this.draw(openData) }, // 定义绘制函数 draw(openData) { const textArea = { x: 0, y: 0, width: canvas.width, height: canvas.height * 0.3 }; ctx.clearRect(textArea.x, textArea.y, textArea.width, textArea.height); ctx.fillStyle = "rgba(36, 255, 255, 0.5)"; ctx.fillRect(textArea.x, textArea.y, textArea.width, textArea.height); ctx.font = '16px serif'; ctx.fillStyle = 'black'; ctx.textAlign = 'left'; // 设置文本对齐方式为左对齐 ctx.fillText(openData, textArea.x, textArea.y + textArea.height / 1.5); }, created() { canvas = this.getCanvas(); ctx = canvas.getContext('2d'); canvas.width = 300; canvas.height = 300; const that = this; // 区域定义 const loadingArea = { x: 0, y: canvas.height * 0.3, width: canvas.width, height: canvas.height * 0.7 }; const centerX = canvas.width / 2; const centerY = canvas.height / 2; const radius = canvas.width / 10; let angle = 0; // Colors and labels const colors = ['#3498db', '#e74c3c', '#f1c40f', '#2ecc71']; const labels = ['redirectTo', 'exit full', 'exit not full', 'hide 2s show 2s', 'updateSize']; const squares = []; tt.onTouchEnd(handleClick) tt.onTouchStart((touch) => { console.log('touchstart=======', touch) }) tt.onTouchCancel((touch) => { console.log('touchcancel=======', touch) }) function handleClick(event) { const { changedTouches: [{ clientX: x, clientY: y }], } = event; for (let i = 0; i < squares.length; i++) { const square = squares[i]; if ( x >= square.x && x <= square.x + square.size && y >= square.y && y <= square.y + square.size ) { switch (i) { case 0: tt.redirectTo({ url: '/pages/second/index', success: (res) => { console.log('拉起面板成功') }, fail: (res) => { console.error('拉起面板失败') }, }); break; case 1: tt.exitMiniProgram({ isFullExit: true, success: (res) => { console.log('完全退出小程序成功') }, fail: (res) => { console.log('完全退出小程序失败') }, }); break; case 2: tt.exitMiniProgram({ isFullExit: false, success: (res) => { console.log('小程序退后台成功') }, fail: (res) => { console.log('小程序退后台失败') }, }); break; case 3: that.hide(); setTimeout(() => { that.show() }, 2000); break; default: that.updateSize({ width: 50, height: 50, success() { console.log('updateLiveCard 成功!!!!') }, fail(res) { console.log('updateLiveCard 失败', res) } }); break; } break; } } } // Hide and Show Logic function handleHideShow() { canvas.style.display = 'none'; setTimeout(() => { canvas.style.display = 'block'; }, 2000); } // Animation function function drawLoading() { ctx.clearRect(loadingArea.x, loadingArea.y, loadingArea.width, loadingArea.height); squares.length = 0; // Clear the squares array ctx.fillStyle = "rgba(36, 255, 255, 0.5)"; ctx.fillRect(loadingArea.x, loadingArea.y, loadingArea.width, loadingArea.height); // Draw Loading Text ctx.fillStyle = '#000'; ctx.font = `${radius * 0.5}px Arial`; ctx.textAlign = 'center'; ctx.fillText('Loading', centerX, centerY - radius * 1.5); // Adjusted position // Draw the rotating circle ctx.beginPath(); const x = centerX + Math.cos(angle) * radius; const y = centerY + Math.sin(angle) * radius; ctx.arc(x, y, radius * 0.3, 0, Math.PI * 2); // Adjusted size ctx.fillStyle = colors[0]; ctx.fill(); ctx.closePath(); // Draw the squares and labels const squareSize = radius * 1.8; const padding = radius * 0.8; for (let i = 0; i < labels.length; i++) { const row = Math.floor(i / 3); const col = i % 3; const posX = centerX - squareSize * 2 + col * (squareSize + padding); const posY = centerY - radius * 2 + row * (squareSize + padding); // Draw the square ctx.fillStyle = colors[i % colors.length]; ctx.fillRect(posX, posY, squareSize, squareSize); // Store square details for click detection squares.push({ x: posX, y: posY, size: squareSize }); // Draw the labels below the square ctx.fillStyle = '#000'; ctx.font = `${radius * 0.3}px Arial`; ctx.textAlign = 'center'; ctx.fillText(labels[i], posX + squareSize / 2, posY + squareSize + radius * 0.6); } angle += 0.05; if (angle > Math.PI * 2) { angle = 0; } } // Start the animation drawLoading(); this.draw(''); } })

卡片数据获取

开启卡片后,插件会拥有获取直播间开放数据的能力,该能力需要在「开放平台」申请。
开放数据分为两种,「直播间消息」和「直播间接口」,消息需要订阅和监听,接口则是直接请求和响应。
    消息订阅/取消订阅
tt.subscribeLiveInteractPluginMessage(options); // 订阅 tt.unsubscribeLiveInteractPluginMessage(options); // 取消订阅
options 为 object 类型,属性如下:
属性名
类型
默认值
必填
说明
最低支持版本
messageType
MessageType[]
需要订阅的开放消息类型
success
function
接口调用成功的回调函数
fail
function
接口调用失败的回调函数
complete
function
接口调用结束的回调函数(调用成功、失败都会执行)
MessageType 枚举如下:
MessageType
说明
对应能力申请
live_like
获取点赞互动数据
live_comment
获取直播间评论互动数据
live_gift
获取礼物互动数据
live_follow
获取直播间观众关注状态
live_fansclub
获取粉丝团互动数据
    消息接收
tt.onReceiveLiveInteractPluginMessage(callback);
callback 定义如下:
类型
默认值
必填
说明
最低支持版本
function
收到消息后触发的回调函数
回调函数的参数为 Payload[] 类型,Payload 是个 object,属性如下:
属性名
类型
说明
msg_id
string
消息 id,通用返回
timestamp
number
时间戳,通用返回
sec_open_id
string
用户的加密 openid,通用返回
avatar_url
string
用户头像,通用返回
nickname
string
用户昵称,通用返回
like_num
number
点赞数量
监听 live_like 消息返回(需要开启「获取点赞互动数据」权限)
content
string
评论内容
监听 live_comment 消息返回(需要开启「直播间评论互动数据」权限)
gift_num
number
送出的礼物数量
监听 live_gift 消息返回(需要开启「获取礼物互动数据」权限)
use_follow_action
number
用户关注变更数据 1 关注 2 取关
监听 live_follow 消息返回(需要开启「直播间观众关注状态」权限)
fansclub_reason_type
number
粉丝团变更原因 1-升级、2-加团、16-退团
监听 live_fansclub 消息返回(需要开启「获取粉丝团互动数据」权限)
fansclub_level
number
粉丝团等级
需要开启「直播间观众粉丝团详细信息」权限后额外返回
user_privilege_level
number
用户荣誉等级
需要开启「直播间观众荣誉等级」权限后额外返回
is_follow_anchor
boolean
用户是否关注主播
需要开启「直播间关注互动数据」权限后额外返回
IDE 也提供了对应的消息模拟能力:
一个例子:
// 消息订阅,messageType 是个数组,可以按需订阅 tt.subscribeLiveInteractPluginMessage({ messageType: ['live_like', 'live_comment', 'live_gift', 'live_follow', 'live_fansclub'], success: (res) => { console.log('消息订阅成功'); // 在订阅成功的回调中,去开启消息通道监听 tt.onReceiveLiveInteractPluginMessage((payload) => { // 实际的消息返回,只有申请了对应的开放数据权限,才会返回对应的消息、接口数据 console.log('payload:', payload); }) }, fail: (error) => { console.log('消息订阅失败:', error); } }) // 消息取消订阅,messageType 也是个数组,可以按需取消订阅 tt.unsubscribeLiveInteractPluginMessage({ // 取消订阅评论消息,上面的 onReceiveLiveInteractPluginMessage 回调则不会返回评论消息数据 messageType: ['live_comment'], success: (res) => { console.log('取消订阅成功:', res); }, fail: (error) => { console.log('取消订阅失败:', error); } })

卡片与页面通信

因为在 Page 中,tt.createLiveCard 成功回调获取的 CardContext 实例与 Card 内 this 实际指向同一个实例,因此我们可以通过以下方式进行 Page 和 Card 的通信。
在 Card 内声明一个函数(名称可自定义),该函数会被挂在 CardContext 实例上。
// live-card/card.js Card({ onPageMessage: function(message) { console.log(`玩法卡片收到消息:${message}`); // 接收数据做逻辑处理 } })
在 Page 中通过调用该函数,就可以从 Page 向 Card 传递数据了。
// pages/index/index.js const app = getApp() Page({ createLiveCard: function(){ tt.createLiveCard({ url: '/live-card/card', width: 300, height: 300, success: async (res) => { console.log('调用成功:', res.cardContext); app.globalCanvas = res.cardContext; }, fail: (res) => { console.log('调用失败:', res); }, }) }, postMessageToLiveCard: function() { app.globalCanvas.onPageMessage('页面给玩法卡片发送消息了'); // 发送数据 } })
类似地,也可以在 Page 中在 CardContext 上声明一个函数,在 Card 内调用,实现从 Card 向 Page 传递数据。

cocos 引擎适配

Card 部分支持 cocos 引擎产物,但需要额外做如下适配:
    1.将所有引擎产物内容都放在小程序根目录下;
    2.在 Card 构造器 created 生命周期内 require 产物中的 game.js;
    3.Cocos 引擎中,会通过 tt.getSystemInfoSync 获取 Card 对应 canvas 的宽、高,基于宽高值完成手势处理等逻辑。但在互动插件小程序中,tt.getSystemInfoSync 返回的是系统信息,获取的是设备尺寸的宽、高,cocos 引擎基于这个值做处理会导致手势响应异常。因此建议在 App 启动后,存储小程序中的 tt.getSystemInfoSync 挂在全局方法上,在页面等逻辑内使用这个初始的 JS API。在执行 cocos 引擎产物前,将 tt.getSystemInfoSync 返回值中的 screenWidthscreenHeightwindowWidthwindowHeight 修改为玩法卡片容器的宽高。
// app.js App({ onShow: function () { this.originalGetSystemInfoSync = tt.getSystemInfoSync; // 存储原始的方法,挂在全局供后续使用 } })
// pages/index/index.js const app = getApp() Page({ getSystemInfo: function() { const res = app.originalGetSystemInfoSync(); // 页面中使用时,使用全局上挂的方法 }, })
// live-card/card.js const app = getApp() Card({ created: function(options) { tt.getSystemInfoSync = () => { const res = app.originalGetSystemInfoSync(); res.screenWidth = options.width; res.screenHeight = options.height; res.windowWidth = options.width res.windowHeight = options.height return res; }, require('../game.js'); // require cocos 引擎产物中的 game.js } })
    4.不建议使用 cocos 引擎产物的开发者,在互动插件小程序中调用 CardContext.updateSize 更新玩法挂件宽高或直接更新玩法挂件 canvas 宽高,会出现手势响应异常问题。

测试插件

开发完成后,需要在真实抖音直播间调试/测试,可以参考「互动工具调试教程
    1.在「抖音开放平台」开发配置页使用抖音 App 扫码绑定「调试成员」
    2.打开抖音 App,登录调试成员账号,开启直播间,打开调试悬浮球
    3.在 IDE 点击「预览」or「真机调试」,生成调试二维码,使用直播间「调试悬浮球」扫调试二维码,然后开始测试
测试完毕,后面开始上传插件。

上传插件

    1.在 IDE 中点击上传按钮,填写「版本」和「更新日志」,确认上传
    2.在「抖音开放平台」版本管理页面,查看上传的测试版本包
    3.确认测试没问题后,点击提交审核,若提交前未完善基础信息,会弹 Toast 提示去完善
    4.若已完善基础信息,则弹出抽屉确认提审信息
填写完毕后提交审核成功后,即到审核阶段。

审核插件

审核插件是抖音内部运营审核同学做审核,需要一些时间,请开发者耐心等待。
等待审核通过后可以发布插件,期间可以撤回审核。

发布插件

    1.发布插件前,需要先在「付费设置」页配置插件付费模式
    2.付费设置完成后,回到版本页面,点击「立即发布」,会弹窗二次确认,尽量避免高峰期上线。
    3.确认发布成功后,可以查看线上版本信息
    4.总览中也能看到包版本信息
    5.需要注意,一旦审核完或者上线后,产物配置就不允许再变更了!开发者要提前选择好开发产物类型。

使用插件

    1.主播使用抖音 App 开播后,打开互动面板,选择刚才开发的小程序(这里以 PK 挑战为例)
    2.如果未设置付费,则主播可以直接打开使用;如有付费,则需要购买使用
    3.点击「打开」互动插件,则进入到开发者开发的玩法面板,点击开启玩法,展示玩法卡片
    4.观众会根据玩法,做对应「点赞」、「评论」、「送礼」等互动行为,与主播参与互动