# 腾讯视频互动视频 | 开发指南
# 起步
# 简介
创作平台上线以后,创作者提出了非常多新颖的互动组件需要平台支持,所以平台开放出互动开放平台,创作者可以根据这篇文档开发自定义互动组件并发布到创作平台上
两个平台的功能需要明确
- 开放平台 平台职责是生产组件,创作者在该平台开发组件
- 创作平台 平台职责是生产互动剧,创作者将组件配置到互动剧中
# 快速上手
点击这里,新建一个组件 (仅支持qq登陆,暂不支持微信登录)
1 填写组件名称
2 上传封面图
3 点击运行按钮
运行后可以看到如下的效果:
# 开发自定义组件
本教程以开发气泡组件为例。 最终效果如下:
组件开发分为两部分:
组件属性配置 组件的哪些部分是可配置的
组件代码编写 组件内部的逻辑功能实现
# 组件表单配置
比如气泡组件,需要配置的属性包括:
x 气泡(左上角)坐标
y 气泡(左上角)坐标
width 气泡宽度
height 气泡高度
text 气泡内容
那么,我们需要如下的表单去配置一个气泡:
生成表单的功能集中在界面右侧,分为 配置属性 和 预览表单 两个Tab。顾名思义,表单需要的控件通过 配置属性 来完成,配置后即可 预览表单
先配置属性:
创建完后,切换到预览表单,可以看到表单生成好了:
注意
系统内置了下面几个属性,不需要额外配置:
x 组件(左上角)坐标
y 组件(左上角)坐标
width 组件宽度
height 组件高度
startTime 组件出现时间
endTime 组件消失时间
# 组件代码编写
代码编辑器本身采用monaco-editor,参考 vscode快捷键
表单已经开发完了,那么组件如何去引用表单里的数据?下面是一张示意图:
每一个属性都对应着vue组件里面的一个prop属性。为了方便开发,开发者不需要再给vue组件写props。组件会根据表单的属性自动生成props,并注入到组件中。
当然,系统内置的属性(x, y, width, height, startTime, endTime)也会以prop属性注入到组件中,开发者无需重复定义。
接下来实战一下。 系统默认会提供了一段初始代码:
<template>
<div
class="container"
v-show="visible"
:style="{
top: y + '%',
left: x + '%',
width: width + '%',
height: height + '%'
}">
Hello Component!
</div>
</template>
<script>
export default {
mounted() {
this.show();
}
};
</script>
<style>
.container {
position: absolute;
}
</style>
注意
组件默认是使用绝对定位,如果组件有特殊需求,可以调整为其他样式
组件使用系统内置属性x, y, width, height设置位置和大小,这样组件的大小和位置就可以由表单控制。(组件也可以不使用系统内置属性,视具体需求而定。)
visible是组件内置的状态值,表示组件此时的显隐情况
show是组件内置的api函数,调用后visible=true。
基于初始代码,开发气泡组件,开发步骤分为:
样式开发
逻辑开发
样式开发 需要修改html和css:
<template>
<div
class="bubble"
v-show="visible"
:style="{
top: y + '%',
left: x + '%',
width: width + '%',
height: height + '%'
}">
<span class="bubble-text">{{text}}</span>
</div>
</template>
<script>
// ...
</script>
<style>
.bubble {
position: absolute;
display: table;
border-radius: 25px;
background-color: rgba(0, 0, 0, 0.5);
border: 1px solid white;
text-align: center;
}
.bubble-text {
display: table-cell;
vertical-align: middle;
}
</style>
注意
点击“运行”按钮,或者按快捷键ctrl+r(mac也可以为command+r),即可运行组件。
效果如下:
在预览表单 Tab输入气泡内容,播放器会同步更新数据哦~
关闭播放器,开始逻辑开发。逻辑功能是用户点击,气泡消失。所以只需要增加一个点击事件让组件消失即可,实现代码如下:
<template>
<div
class="bubble"
@click="onClick"
...>
...
</div>
</template>
<script>
export default {
mounted() {
this.show();
},
methods: {
onClick() {
this.hide();
}
}
};
</script>
再次运行,气泡就可以点击关闭了
# 运行调试
在当前页面载入播放器运行的方式能快速的看到组件开发的效果,但是却无法调试移动端特性,例如touch事件无法调试 于是系统提供了三种运行方式供开发者调试,开发者按需使用:
本页面直接运行(不再累述)
通过手机扫码,在手机浏览器上运行
- step1
- step2
- step3 手机调试支持livereload,直接修改代码,点击运行,手机就会自动刷新
新开浏览器标签页运行
- 流程和移动端差不多
# 进阶指南
进阶指南主要介绍系统提供的附加功能的使用
# 组件显示隐藏控制
# • 必须使用show/hide函数
上述的demo代码中,频繁出现this.show/hide的调用,这两个函数就是组件显示/隐藏的控制开关。但为什么要调用这个函数去控制组件显示/隐藏,而不是直接控制顶层div的样式?原因如下: 如上图,绿色箭头表示一个视频随着时间往后播放。而这个视频上有两个组件,组件的花括号表示组件的展示的时间段。蓝色箭头表示组件触发show或者hide函数,这其实是在告诉互动播放器,在组件不展示的时间段内,播放器可以展示进度条以及其他播放器相关的UI。简而言之,组件和播放器上的控件是互斥的。
那么,组件不调用show或hide会带来什么问题?假如上图组件1没有调用hide,那么互动播放器就会认为组件1一直没有消失,播放器进度条就展示不出来,如下图:
注意
注意:这就是为什么明明组件div已经隐藏了,但是互动播放器上依然点不出进度条点原因!!!
# • 平台建议写法
所有组件都要依赖组件内置的属性visible来控制显示和隐藏,且要通过show/hide两个函数控制visible属性的变化。举个例子:
<template>
<div v-show="visible"></div>
</template>
<!-- 如果组件需要渐变动画 -->
<template>
<div class="mycls" :style="{opacity: visible ? 1 : 0}" />
</template>
<style>
.mycls {
transition: opacity .2s ease;
}
</style>
show/hide两个函数除了改变visible的值,还会把组件显隐状态通知给互动播放器。
# 自定义组件销毁时机
组件默认是在当前视频播放结束后销毁,但如果组件希望在视频跳转时展示一个过渡效果,那么就可以自定义组件的销毁时机。
# • 如何自定义组件销毁时机
自定义组件销毁时机分为两个步骤: ● 通知引擎在视频结束时,不要销毁此组件 ● 组件到了需要销毁的时机,通知引擎销毁它 代码如下
<template>
<div
class="container"
:style="{
top: y + '%',
left: x + '%',
width: width + '%',
height: height + '%',
opacity: visible ? 1 : 0
}"
@click="onClick"
>
点我跳转
</div>
</template>
<script>
export default {
mounted() {
this.show();
// 步骤1 通知引擎在视频结束时,不要销毁当前组件
this.$emit('interruptdestroy');
},
methods: {
onClick() {
// action是配置跳转行为
this.bridge.action(this.action);
this.visible = false;
setTimeout(() => {
// 步骤2 通知引擎销毁当前组件
this.$emit('requestdestroy');
}, 3000);
}
}
};
</script>
<style>
.container {
position: absolute;
transition: opacity 3s ease;
background: red;
}
</style>
效果如下:
# 属性的配置
- 属性名 默认会自动生成,但建议改成业务相关的变量名,便于理解
- 类型 决定属性的数据结构以及表单控件的类型
- 属性含义 表单控件对应的label文本,方便用户理解表单的含义。例如:
# 素材列表的使用
开发组件的过程中,可能需要在代码中使用图片或音频。系统提供了素材列表的功能,开发者可以上传素材,并复制链接到相应的代码区。
素材列表的入口在界面右上方
打开素材列表后的界面如下,使用教程:
# 第三方js/css库的引用
第三方js/css库的引用的功能在素材列表功能中。
# • 创建库
以elementui为例。elementui需要引入elementui的js和css,将它们上传到平台。 所有上传的脚本有个审核的过程,脚本审核通过后就可以正常使用了。(PS:审核通过后需要重新打开素材列表面板才会刷新列表。)
创建好的库可以在其他组件中使用,无需重复上传,运行时也不会重复加载同一个库
# • 使用库
平台提供了两种库的引用的方式:
- 初始化时加载 用户打开互动播放器时就会加载此脚本(默认)
- 按需加载 用到该组件时才会加载相应的库
引用库的方式:
- 选中需要的库
- 关闭素材列表面板
- 点击运行即可生效
素材列表选择示例图:
组件代码(就是加了el-button,其余代码省略):
<template>
<div class="container" ....>
<el-button>Button</el-button>
</div>
</template>
<script>
// ...
</script>
运行结果:
由于elementui的vue组件必须在vue实例化之前注册,所以elementui不能用按需加载,否则elementui的组件将不会生效,其他的库视情况而定
# 组件控制视频跳转
- 在配置属性tab新增属性action,类型为行为
- 在预览表单tab设置视频跳转到测试视频1,在实际互动剧中,这里可以选择故事线中的视频
写一个简单的点击跳转逻辑
<template>
<div
class="container"
:style="{
// ...省略
}"
@click="onClick"
>click me!!!
</div>
</template>
<script>
export default {
mounted() {
this.show();
},
methods: {
onClick() {
// this.action则是上面表单配置的内容
this.bridge.invoke('engine', 'action', this.action);
}
}
};
</script>
<style>
.container {
position: absolute;
}
</style>
# 数值的使用
我们引入了数值体系的概念来解决组件间通信的问题:
由图可以看出,图中有两个数值,一个叫 apple,一个叫 banana,组件4监听数值 apple 的变化,而 组件1 通过某种交互触发 apple=apple+1 ,从而修改了 apple 的值,于是数值体系就会将 apple 的变化通知给 组件4 ,达到通信的效果。 数值相关的JSApi(参考JSApi使用指南)有:
action 执行数值表达式,从而修改数值
listenValue 监听某个数值的变化
listenCondition 监听某个条件表达式的变化,举例:apple > 1
# • 如何监听数值
实践一下,举个背包物品组件的例子说明数值的使用方法。假设一个场景,有个物品是苹果,UI上需要实时展示苹果数量。步骤如下:
在 配置属性 Tab新增属性vkey,类型为 数值
在 预览表单 Tab新增一个数值叫苹果,并且把属性vkey设置为苹果
- 组件代码部分监听数值变化,并把数量展示在UI上
<template>
<div
class="container"
:style="{
top: y + '%',
left: x + '%',
width: width + '%',
height: height + '%',
backgroundImage: `url(http://ivimgbucket-30295.sz.gfp.tencent-cloud.com/ivopenplatimg/1061215784665206140642842FA3775A8292C1083B321E4223778464.png)`
}"
>
{{ count }}
</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
mounted() {
this.show();
this.bridge.listenValue(this.vkey, val => {
this.count = val;
});
}
};
</script>
<style>
.container {
position: absolute;
background-size: 100% 100%;
text-align: center;
font-size: 500%;
line-height: 155px;
}
</style>
效果如下:
注意
例子中的数值列表是开发组件的时候调试用的,实际互动作品创作需要在作品创作平台创建。
数值外显名称只是用来UI显示用的,数值实际的变量名是系统会自动为它生成的,开发者无需关心。
# • 如何修改数值
那么组件如何去修改苹果数量呢?为了方便演示,把修改数值和监听数值都做在同一个组件里。 在上面的例子的基础上加点功能:点击物品,物品的数量会加1。修改步骤如下:
在 配置属性 Tab新增属性action,类型为 行为
在 预览表单 Tab设置行为的值为 苹果+1
- 代码修改如下(篇幅有限,所以用省略号代替部分代码):
<template>
<div
...
@click="plus"
>
{{ count }}
</div>
</template>
<script>
export default {
...
methods: {
plus() {
this.bridge.invoke('engine', 'action', this.action);
}
}
};
</script>
效果如下:
# • 如何监听数值条件
再次改造上述例子,演示数值条件的使用,场景为:当数值大于3时,字体变红。修改步骤如下:
在 配置属性 Tab新增属性 condition,类型为 数值条件
在 预览表单 Tab设置行为的值为 苹果>3
- 代码修改如下(篇幅有限,所以用省略号代替部分代码):
<template>
<div
...
:style="{
...
color: red ? 'red' : ''
}"
...
>
{{ count }}
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
red: false
};
},
mounted() {
...
this.bridge.listenCondition(this.condition, red => {
this.red = red;
});
},
...
};
</script>
效果如下:
# • 变量类型
数值分为:
普通变量
永久变量
区别在于:
普通变量随着存档,存档删除了,普通变量会重置
永久变量则相反,存档删除不会重置永久变量,例如成就/称号这样的可以用永久变量实现
# • 创建故事线
需要开发者自己实现一个 story.js
,需要实现以下功能:
在window上挂载一个全局构造函数VideoInteractOverview
, 并实现以下接口:
// 以es6为例,需要最终转换成es5的代码
export default class VideoInteractOverview {
/**
* @construct
* @param conf {Object} 初始化参数
* @param conf.mode {String} 请求后台接口的模式. 默认: '', 可选: 'test',当传入test时,希望请求后台测试接口
* @param conf.el {String} 故事线被挂载的dom节点选择器。例如: '#overview'
* @param conf.appId {String} 互动剧的appid
* @param conf.dramaId {String} 互动剧的剧id
* @param conf.source {String} 预览模式时需要带上的请求字符串. 默认: '', 可选值: 'preview'
* @param conf.preview_session {String} 预览模式时需要带上的session. 默认: ''
**/
construct(conf) {
// 在这里实现初始化故事线的逻辑
}
/**
* @name show
* @description 外部调用展示故事线,例如: 故事线按钮
**/
show() {
// 在这里实现渲染故事线的逻辑
},
/**
* @name hide
* @description 外部调用隐藏故事线,例如: 故事线按钮
**/
hide() {
// 在这里实现隐藏故事线的逻辑
},
/**
* @name onClose
* @description 外部监听故事线内部的关闭事件
* @param callback { Function } 故事线关闭时需要触发的回调函数
**/
onClose(callback) {
// 在这里将callback加入到关闭事件的回调队列中
},
/**
* @name onSelectView
* @description 外部监听故事线选择某个分支剧情事件
* @param callback { Function } 故事线选择某个分支剧情时需要触发的回调函数
**/
onSelectView(callback) {
// 在这里将callback加入到点击分支剧情事件的回调队列中
// 回调函数需要传入以下参数:
// {
// branch_id, // 表示当前选择的分支id
// video_id, // 当前视频的vid
// }
// 例如
// callback({branch_id: 12345678, video_id: 'abcdefg'})
},
/**
* @name selectBranch
* @description 外部调用切换分支剧情
* @param conf { Object } 切换分支剧情的参数
* @param conf.branch_id 分支剧情的id
**/
selectBranch(conf) {
// 在这里实现切换分支后渲染新的分支的逻辑
}
}
# 组件引用
组件可以引用其他的组件,组件的引用也是严格遵循vue的语法。本节依然是举例介绍组件引用。
# • 简单引用
例子:写一个组件,引用官方组件“气泡”,使“气泡”出现的时候有个淡入动画,且气泡内容可以由外部设置。步骤如下:
在 配置属性 Tab新增属性child,类型为 组件 ,组件为 气泡
在预览表单tab设置子组件的气泡内容为:这是一个淡入的气泡
- 代码如下:
<template>
<div
class="container"
:style="{
top: y + '%',
left: x + '%',
width: width + '%',
height: height + '%',
opacity: visible ? 1 : 0
}"
>
<V1392548344947063822
v-bind="child"
:startTime="startTime"
:endTime="endTime"
:width="100"
:height="100"
/>
</div>
</template>
<script>
export default {
mounted() {
this.bridge.on('player', 'timeupdate', time => {
if (time >= this.startTime && time <= this.endTime) {
this.show();
} else {
this.hide();
}
});
}
};
</script>
<style>
.container {
position: absolute;
transition: opacity 1s ease;
}
</style>
效果如下:
注意
父组件可以把子组件的普通属性配置child通过v-bind传递给子组件;而内置属性需要根据具体情况设置给子组件 PS:内置属性包括(x,y,width,height,startTime,endTime)
# • 引用多个同类型组件
例子:写一个组件,引用官方组件“气泡”,使组件内部可以展示多个气泡,气泡数量和内容可以由外部设置。步骤如下:
在 配置属性 Tab新增属性children,类型为 多组件,组件为 气泡
在 预览表单 Tab设置子组件的气泡内容为:气泡1
- 代码如下:
<template>
<div
class="container"
:style="{
top: y + '%',
left: x + '%',
width: width + '%',
height: height + '%'
}"
>
<V1392548344947063822
v-for="(child, i) in children"
v-bind="child"
:startTime="startTime"
:endTime="endTime"
:x="i * 100 / children.length"
:width="100 / children.length"
:height="100"
/>
</div>
</template>
<script>
export default {
mounted() {
this.show();
}
};
</script>
<style>
.container {
position: absolute;
}
</style>
效果如下:
# JSApi使用指南
在组件中,开发者可以通过 this.bridge 提供的接口和事件,实现更灵活的逻辑。下面提供一个简单的demo。
// 视频播放的第10s的时候执行action
export default {
data() {
return {
hasAction: false
};
},
mounted() {
this.bridge.on('player', 'timeupdate',(time)=>{
if(time >= 10 && !this.hasAction) {
this.bridge.invoke('engine', 'action', this.action);
this.hasAction = true;
}
})
}
};
# 常用API文档
- action
/**
* @description 通过action方法,执行配置的“行为”
* {Object} actions 行为
* {
* jump, // 分支切换
* expression // 执行表达式
* }
*/
this.bridge.invoke('engine', 'action', actions) // 传入需要执行的行为
- pause
/**
* @description 暂停当前播放
*/
this.bridge.invoke('player', 'pause');
- play
/**
* @description 继续当前播放
*/
this.bridge.invoke('player', 'play');
- seek
/**
* @description 通过seek方法,跳到视频的指定进度
* {Number} time 单位 秒
*/
this.bridge.invoke('player', 'seek', time);
- listenValue
/**
* @description 监听数值变化
* {String} vname 需要监听的v数值
* {Function} callback 变化触发的回调函数 绑定时会马上执行一次
*/
this.bridge.listenValue(vname, callback)
// 监听数值变化,例如在上一章节中设置一个数值属性,属性名为apple
this.bridge.listenValue(this.apple, (value)=>{
console.log('数值apple发生变化', value)
})
- listenCondition
/**
* @description 监听条件判断表达式结果变化
* {String} condition 需要监听的条件判定
* {Function} callback 变化触发的回调函数 绑定时会马上执行一次
*/
this.bridge.listenCondition(condition, callback)
// 监听数值变化,例如在上一章节中设置一个数值条件属性,
// 属性名为appleEnough
this.bridge.listenCondition(this.appleEnough, (result)=>{
console.log('apple 足够了', result)
})
- px2Percent
/**
* @description 传入宽高px值 换算成相对于视频宽高的百分比
* {Object} options 宽高信息
* {
* x, // 宽
* y, // 高
* }
*
*/
this.bridge.invoke('utils', 'px2Percent', options) // 返回Object {x,y} 单位%
- percent2Px
/**
* @description 传入宽高百分比 换算成相对于视频宽高的px值
* {Object} options 宽高信息
* {
* x, // 宽
* y, // 高
* }
*
*/
tthis.bridge.invoke('utils', 'percent2Px', options) // 返回Object {x,y} 单位px
- stateChange
/**
* @description 监听播放状态变化
*/
this.bridge.on('player', 'stateChange',(state)=>{
console.log('当前播放状态是', state);
})
- timeupdate
/**
* @description 监听播放进度变化
*/
this.bridge.on('player', 'timeupdate', (time) => {
console.log('当前播放时间是', time);
});
- videoNodeChange
/**
* @description 监听播放视频发生切换
*/
this.bridge.on('engine', 'videoNodeChange',(videoInfo)=>{
// {streamRatio, 视频宽高比
// vid, 当前播放的vid
// duration 当前视频总时长 单位 ms
// }
console.log('当前播放视频信息', videoInfo);
})
- interactInfoUpdate
/**
* @description 监听互动节点变化
*/
this.bridge.on('engine', 'interactInfoUpdate',(interactInfo)=>{
console.log('当前互动节点信息', interactInfo);
})
# player命名空间完整API
# bridge命名空间完整API
# engine命名空间完整API
# utils命名空间完整API
# 音频播放类完整API
# 组件指令使用
v-fseq/序列帧动画
- 属性配置
// 序列帧表单属性设置可以参考附录2 // 一个设置好的序列帧数据命名为 bg 如下所示 const bg = { // 上传的序列帧图片地址 url: 'http://ivimgbucket-30295.sz.gfp.tencent-cloud.com/ivopenplatimg/413241578642147677805555B68903BC4AA5428A712DB093C3DDD804.png', // 图片的宽 后台计算 width: 400, // 图片的高 后台计算 height: 4400, // 帧数 freq: 11, // 速率 speed: 20, // 是否循环 loop: true, };
- 代码
<template> <div class="container" v-show="visible" :style="{ top: y + '%', left: x + '%', width: width + '%', height: height + '%' }"> <div style="width:100%;height:100%" v-fseq="{ // 传入设置的属性 ...bg // 通过此参数控制序列帧动画是否运行 active: true | false, // 每次循环播放的间隔 单位 秒 interval: 1 // 控制循环间隔时画面停止在第一帧还是最后一帧 默认false 停在最后一帧 isStart: true | false // 控制画面停在某一帧 此时active须设置为false stopsAt: 1 // 对应css animationDirection属性 direction // 对应css animationFillMode属性 fillmode // 对应css animationDelay属性 delay }" /> </div> </template>
# 常见问题
- Q1: 如何判断当前互动节点的视频播放结束?
// 方法一:通过时间变化判断
let duration;
this.bridge.on('engine', 'videoNodeChange',(videoInfo)=>{
duration = videoInfo.duration ;
})
this.bridge.on('player', 'timeupdate',(time)=>{
let miliSec = time * 1000 // 单位统一成毫秒
if(duation > 0 && miliSec >= duration) {
console.log('播放结束')
}
// 由于部分设备取时间不准确 建议采用以下方式 结束前200ms判断为结束。
if(duation > 0 && miliSec >= duration - 200) {
console.log('播放结束')
}
})
// 方法二:通过事件判断 (此功能待发布,预计1月15号)
this.bridge.on('engine', 'videoNodePlayEnd',()=>{
console.log('播放结束')
})
# 附录
# 属性类型
- 文本 {String} 配置组件中的字符串属性
- 数字 {Number} 配置组件中的数字属性
- 开关 {Boolean} 配置组件中的布尔属性
- 枚举 {String} 枚举类型
对应的值是选中的选项的id。选中套餐1,则对应值就是suit1。
- 图片 {Object} 对应的值的数据结构如下:
{
url: 'https://domain/path/pic',
width: 100, // 原始图片的宽
height: 100 // 原始图片的高
}
- 序列帧 {Object} 支持单列竖排的序列帧图片
{
url: 'https://domain/path/pic',
width: 10, // 原始图片的宽
height: 480, // 原始图片的高
freq: 48, // 序列帧图片的帧数
speed: 24, // 序列帧动画播放速度。单位是帧/秒,默认为24
loop: false // 序列帧动画是否循环播放
}
- 音频 {Object} 对应的值的数据结构
{
url: 'https://domain/path/audio'
}
- 数值 {String} 对应的值是数值的变量名,可以通过api监听此变量的值的变化。
- 数值条件 {String} 对应的值是表达式,例如 v123>1 ,可以通过api监听表达式返回值的变化
- 行为 {Object} 包括:视频跳转和数值变更。对应的值的数据结构如下:
{
nextBranch: '123',
nextChapter: '456',
expression: 'a=a+1,b=b-2'
}
一般情况下把这个配置透传给api处理就能自动完成视频跳转或者数值变更。