一、为什么要做这玩意儿?
C端内容创作业务概要
先看一下下C端内容创作业务概要。
UP主创作一个可被大众用户观看到稿件需要经过 灵感构思 → 素材剪辑 → 上传 → 投稿 这些环节,
然后后续流程经B站内部加工完毕后,最终C端用户能浏览观看,该部分场景动作都可以通过B站的集成创作工具来闭环完成。
这些流程专业的细化下来,主要包括。
创作(bilibili粉版创作端、必剪等) → 视频上传(原片上传) → 投稿(封面+标题+简介等) → 转码(分辨率、水印) → 内容安全审核(黄暴恐+版权等) → 稿件开放(完成生产)→ 稿件分发(CDN分发)
UGC与OGV业务场景对比
如上就是一个UGC视频的创作过程,在消费端也主要以单视频呈现为主,没有剧集概念及内含的媒资概念,对于内容的组织度也没那么高的要求,如下为OGV内容呈现方式。
拥有相应频道对内容进行分品类运营的能力,内容内部除了UGC播放页的所有元素还包括相应的媒资信息、社区评分、ep列表、不同的视频分节等元素构成一部剧集内容,
从上也就看出OGV内容相对于UGC内容组织度的区分,在分发上也是以剧集的粒度进行分发。
那么是不是可以直接使用UGC创作端的工具就可以实现OGV内容制作业务场景呢?答案是否定的。
如果要在内容产出上分别用一个词分别描述UGC和OGV的特点,那么UGC是创作,OGV是制作。
UGC的创作端需要许多辅助用户的轻量化模版工具进行创意产出变现,而OGV的内容主要是以外部采购的方式引入后进行剪辑、压轴、添加字幕等处理,使用的工具相对重量化一些,相应的在投稿阶段也会有一些针对OGV场景的定制化要求并且需要掌握整个生产流程,而UGC的链路对于平台来说主要是掌握在投稿阶段,所以直接使用UGC创作端工具不太合适。
整个业务场景在人员视角下处理流程对比如图,图中蓝色虚线圈起来的部分为平台可掌握部分,红色是UP主自主决定部分。
这其中实际面对的问题,
UGC投稿场景用户可以集中在连续界面进行输入,上传/投稿/充电策略操作流程连贯,需输入信息也相对OGV较少,用户提交完之后就不需要关心后续内部处理过程。
OGV内容制作部分由我们的媒资运营处理,需录入相应媒资信息, 然后将原始视频压制字幕、音轨、水印等信息,待完成后上传视频云获取cid(上传完成后获得的视频引用编号),
最后用cid及其他信息投稿主站触发转码产出可用视频物料, 执行过程中需要媒资运营在各个功能系统界面切换,人工关注确定处理进度和回收中间处理结果,并人工进行过程衔接,没有一个统一操作视角。
为了获得播放权利和原始物料还需要对接上游的版权运营,当可用物料产出完毕之后 后续还需交付给频道运营组织内容后进行渠道分发(粉版、ott)。
整个生产过程中人员协作没有成体系都是人找人,中间物料等信息流的交换采用邮件或企微等线下方式。
OGV流程梳理
项目发起时对OGV主要核心流程梳理下来主要分为三部分:由媒资入库、视频生产以及策略绑定组成。
媒资入库:媒资信息的创建、剧集壳的创建
视频生产:产出可用视频物料
策略绑定:剧集付费策略绑定,比如大会员免费
可以看到每个部分的操作流程和其中需要输入信息都是不一样的。
视频物料后续处理流程梳理 :
主要流程执行完毕之后,还存在对视频的后续处理流程,视频物料试看评估如果不符预期那么还需进行人工视频换源流程重新生产,痛苦加倍。
后续还面临新增处理流程的可能性,现在回过头来看确实如此,也就演变成了当前的 视频片头片尾打点、ott路生产、超分、视频质量评估。
大部分的操作流程和输入信息是不一样的,除去部分流程,例如视频生产和视频换源,前面压制、上传、转码的步骤是一样的只是后续的删除分P 转移弹幕 更新剧集分集绑定cid的动作不一样。
问题痛点
综合以上,消费端呈现方式的不同、参与制作角色不同、内容来源不同,最终导致了问题空间的不同。
在业务上需要解决人员协作松散、人工环节多、生产效率低的问题,技术上则是输入信息多样、生产流程多样、可扩展性。
二、实际该怎么做?
在图中虚线分割部分是子领域,实线分割为限界上下文,原则上识别出来的每个子域只对应一个问题,子域之间是相互独立的,没有交叉,没有包含关系。子领域是在整个OGV业务的角度进行识别划分,在这之外还存在一些非强业务相关的推送通知,流程管理,认证验权等通用子领域。理论上子领域仍然可以被分解,例如我们通过对不同内容运营部门的不同角色对内容的运营阶段的需要,将剧集子领域又被分解为剧集内容,排播,策略规则,每个细分的子领域又对应一个该领域内的特定问题。
除了进行子领域的识别和划分,在问题阶段要明确每个子领域核心要解决的问题是什么,例如:剧集的领域是作为OGV内容载体供用户消费,需要解决内容的表达载体,管理,排播,运营等问题。在剧集的细分子领域中:
1.剧集内容的表达,就是要解决剧集,分集,片单的内容关系,以及基于内容的上下架等各种管理,以及用户的消费。
2.细分的排播子领域:主要是用来解决剧集内容的排播计划,一方面让支撑运营管理内容的播放计划,同时让用户可以消费到海量内容的播放计划。
3.策略规则的子领域:主要是面对运营支撑者解决剧集内容在播放控制,分发控制,付费控制等多个业务维度的运营问题。
当我们在定位一个子领域时,会出现其他业务内也存在这样的问题,例如:版权,OGV业务面临版权问题,bilibili的其他业务音乐,漫画等也面临版权问题,此时在另一个的层面,企业维度来看,这个对于OGV来说的版权子领域,在企业多业务角度,版权领域的定位就会发生变化,其要解决的就不仅仅是OGV业务的版权问题,并且随着外部环境变化(政策,行业对版权的规范性变化),领域的重要程度也可能变更,但是并不改变子领域在价值归属,版权领域作为支撑子领域的价值判断。
在看待问题域的子领域时不考虑实现问题,例如图中的稿件,用户,弹幕,评论对于OGV业务而言是核心业务,但是这些子领域在实际中已经有其他非OGV业务的支撑人员和系统可以解决这些领域的问题,所以以灰色示意的方式进行表达。
基于如上原因,相应的为了解决生产过程中的问题复杂性,划分出了生产计划子领域来应对OGV内容生产过程中的问题。
架构设计
业务模型
为了解决人员协作松散的问题,借鉴传统供应链的工作计划模型,基于OGV业务场景抽象了需求、计划、任务模型构建工单系统。
通过系统固化了生产流程和生产人员体系,做到人员对接有迹可循和数据沉淀。
业务模型层面
需求:负责接收版权运营发起的生产委托。
计划:根据需求负责媒资运营的内部人员生产排期。
任务:根据计划负责媒资运营或频道运营具体人员的生产执行。
数据模型
为了应对输入信息和中间过程产生的信息多样性问题,在数据模型层面上采用了非结构化表的设计,
输入材料信息、上下文信息、状态变更流水都是kv型存储结构,
以此达到业务数据的可描述性,以及后期存储的可扩展性, 不用频繁变更数据库字段,降低负面影响。
输入多样性
虽然在数据层面解决了输入多样性的问题,但为了防止输入信息的泛滥和对任务需要的输入信息进行描述。
所以在实现层面实现了输入信息校验器抽象了输入信息规则,做到代码即文档, 输入信息规则描述了具体类型的任务对应有哪些输入信息,对输入进行约束。
在任务发起时统一收口输入校验,并进行详细错误提示,保证了输入与存储正确性。
从而达到增强系统业务描述性、约束性和可扩展性的目的。
下面开始以生产系统服务诸多任务类型中的视频生产任务进行数据和流程举例,
public class TaskVideoProductionEncodeCategory extends AbsAttrRuleCategory {
@Override
protected String initCategoryName() {
return "压制型视频生产任务输入信息规则";
}
@Override
protected List<AttrRule> initAttrRuleList() {
List<AttrRule> attrRuleList = Arrays.asList(
AttrRule.builder()
.attrName("视频类型").attrCode(AttrCode.CATEGORY_ID)
.validateRule(AttrValidateRule.notBlankStrValidate)
.isMust(true).group("archive").order(1)
.build(),
AttrRule.builder()
.attrName("集数").attrCode(AttrCode.EPISODE_NUM)
.validateRule(AttrValidateRule.numberValidate)
.group("archive").order(2)
.build(),
AttrRule.builder()
.attrName("视频标题").attrCode(AttrCode.TITLE)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").isMust(true).order(3)
.build(),
AttrRule.builder()
.attrName("版本").attrCode(AttrCode.EDITION)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").isMust(true).order(4)
.build(),
AttrRule.builder()
.attrName("视频简介").attrCode(AttrCode.OVERVIEW)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").order(5)
.build(),
AttrRule.builder()
.attrName("投稿类型").attrCode(AttrCode.CONTRIBUTE_TYPE)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").order(6)
.build(),
AttrRule.builder()
.attrName("投稿账号").attrCode(AttrCode.MID)
.validateRule(AttrValidateRule.notBlankStrValidate)
.isMust(true).group("archive").order(7)
.build(),
AttrRule.builder()
.attrName("分区").attrCode(AttrCode.TID)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").order(8)
.build(),
AttrRule.builder()
.attrName("投稿通道").attrCode(AttrCode.CONTRIBUTE_CHANNEL)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").isMust(true).order(9)
.build(),
AttrRule.builder()
.attrName("封面").attrCode(AttrCode.COVER)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").isMust(true).order(10)
.build(),
AttrRule.builder()
.attrName("是否加急").attrCode(AttrCode.RESOURCE_LEVEL)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("encode").isMust(true).order(11)
.build(),
AttrRule.builder()
.attrName("待压制视频文件路径").attrCode(AttrCode.SOURCE)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("encode").isMust(true).order(12)
.build(),
AttrRule.builder()
.attrName("编码器").attrCode(AttrCode.VIDEO_ENCODER)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("encode").isMust(true).order(13)
.build(),
AttrRule.builder()
.attrName("视频编码通用参数").attrCode(AttrCode.VIDEO_PARAMETER)
.group("encode").isMust(false).order(14)
.build(),
AttrRule.builder()
.attrName("1st Pass 码率控制参数").attrCode(AttrCode.VIDEO_RC_PARAMETER1_PASS)
.group("encode").isMust(false).order(15)
.build(),
AttrRule.builder()
.attrName("2st Pass 码率控制参数").attrCode(AttrCode.VIDEO_RC_PARAMETER2_PASS)
.group("encode").isMust(false).order(16)
.build(),
AttrRule.builder()
.attrName("1st Pass 编码允许最低码率").attrCode(AttrCode.VIDEO_RC_MIN_RATE)
.validateRule(AttrValidateRule.numberValidate)
.group("encode").isMust(true).order(17)
.build(),
AttrRule.builder()
.attrName("1st Pass 编码允许最高码率").attrCode(AttrCode.VIDEO_RC_MAX_RATE)
.validateRule(AttrValidateRule.numberValidate)
.group("encode").isMust(true).order(18)
.build(),
AttrRule.builder()
.attrName("并行编码数量").attrCode(AttrCode.VIDEO_THREADS)
.validateRule(AttrValidateRule.numberValidate)
.group("encode").isMust(true).order(19)
.build(),
AttrRule.builder()
.attrName("使用Logo").attrCode(AttrCode.VIDEO_LOGO)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").isMust(true).order(20)
.build(),
AttrRule.builder()
.attrName("音频编码参数").attrCode(AttrCode.AUDIO_PARAMETER)
.group("encode").isMust(false).order(21)
.build(),
AttrRule.builder()
.attrName("音频标准化方式").attrCode(AttrCode.AUDIO_NORMALIZE)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("encode").isMust(true).order(22)
.build(),
AttrRule.builder()
.attrName("是否需要自动上传").attrCode(AttrCode.IS_NEED_AUTO_UPLOAD)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("upload").isMust(true).order(23)
.build(),
AttrRule.builder()
.attrName("待上传视频文件绝对路径").attrCode(AttrCode.FILES)
.group("upload").isMust(false).order(24)
.build(),
AttrRule.builder()
.attrName("视频云转码优化").attrCode(AttrCode.X_CODE_OPT)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("upload").isMust(true).order(25)
.build(),
AttrRule.builder()
.attrName("视频预览时长").attrCode(AttrCode.PREVIEW)
.group("upload").isMust(false).order(26)
.build(),
AttrRule.builder()
.attrName("视频云转码优先级").attrCode(AttrCode.PRIORITY)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("upload").isMust(true).order(27)
.build(),
AttrRule.builder()
.attrName("是否预分发").attrCode(AttrCode.PRE_DISPATCH)
.group("upload").isMust(true).order(28)
.build(),
AttrRule.builder()
.attrName("是否上传为加密视频").attrCode(AttrCode.DRM)
.group("upload").isMust(true).order(29)
.build(),
AttrRule.builder()
.attrName("附带音轨选项").attrCode(AttrCode.FEATURE)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("upload").isMust(true).order(30)
.build(),
AttrRule.builder()
.attrName("生产来源").attrCode(AttrCode.PRODUCE_SOURCE)
.validateRule(AttrValidateRule.notBlankStrValidate)
.group("archive").isMust(false).order(31)
.build(),
AttrRule.builder()
.attrName("X_CODE_QUALITY").attrCode(AttrCode.X_CODE_QUALITY)
.group("upload").isMust(false).order(32)
.build(),
AttrRule.builder()
.attrName("是否超分").attrCode(AttrCode.IS_NEED_SUPER_RESOLUTION)
.isMust(false).order(33)
.build(),
AttrRule.builder()
.attrName("超分优先级").attrCode(AttrCode.SUPER_RESOLUTION_PRIORITY)
.isMust(false).order(34)
.build(),
AttrRule.builder()
.attrName("超分内容类型").attrCode(AttrCode.SUPER_RESOLUTION_TYPE)
.isMust(false).order(35)
.build()
);
return attrRuleList;
}
}
PS:大家可以看到相对于C端创作端投稿来说,B端在进行视频生产时所需要的信息也更多更复杂。
流程多样性
为了解决任务处理流程多样性的问题以及提高人员效率,在实现层面实现了抽象状态机。
也是基于代码即文档的原则, 具体任务只需定义自己的规则,用于描述任务有哪些状态,相应的可流转状态,推进时的前置处理以及推进后的后置处理,
即可保证任务的向正确的状态进行流转运行,并根据自定义推进处理器实现不同业务场景,在有共性的情况下还能做到复用。
(比如之前提到的视频生产流程和换源流程,前面的推进处理器可以复用,后面换源的可以自己实现自己的)
达到增强系统业务描述性、约束性、可扩展性和人员提效的目的。
整个状态机处理器的模型都遵循如下机制,
相对应的支持任务自动推进、后进先出的任务优先级执行队列等能力。
/**
* 视频物料生产任务状态机规则
*/
public class TaskVideoProductionEncodeStatemachine extends AbsStatemachine {
@Override
protected List<StatemachineNode> initStatemachineNodeList() {
List<StatemachineNode> statemachineNodes = Arrays.asList(
StatemachineNode.builder()
.status(Status.WAIT_ENCODE)
.desc("待压制")
.nextStatus(Arrays.asList(
NextStatus.builder()
.status(Status.ENCODING).pushHandler(Handler.ENCODING).pushPostHandler(PostHandler.ENCODING)
.build(),
NextStatus.builder()
.status(Status.CANCEL).pushHandler(Handler.CANCEL).pushPostHandler(PostHandler.CANCEL)
.build()))
.isInit(true).isFinal(false).isPositive(true).isCouldUpdateMaterial(true)
.materialGroupUpdateRangeList(Arrays.asList("encode", "upload", "archive"))
.build(),
StatemachineNode.builder()
.status(Status.ENCODING)
.desc("待压制完成")
.nextStatus(Arrays.asList(
NextStatus.builder()
.status(Status.WAIT_UPLOAD).pushHandler(Handler.WAIT_UPLOAD).pushPostHandler(PostHandler.WAIT_UPLOAD)
.build(),
NextStatus.builder()
.status(Status.ENCODE_FAIL).pushHandler(Handler.ENCODE_FAIL).pushPostHandler(PostHandler.ENCODE_FAIL)
.build(),
NextStatus.builder()
.status(Status.CANCEL).pushHandler(Handler.ENCODING_CANCEL).pushPostHandler(PostHandler.ENCODING_CANCEL)
.build()))
.isInit(false).isFinal(false).isPositive(true).isCouldUpdateMaterial(true)
.materialGroupUpdateRangeList(Arrays.asList("upload", "archive"))
.build(),
StatemachineNode.builder()
.status(Status.WAIT_UPLOAD)
.desc("待上传")
.nextStatus(Arrays.asList(
NextStatus.builder()
.status(Status.UPLOADING).pushHandler(Handler.UPLOADING).pushPostHandler(PostHandler.UPLOADING)
.build(),
NextStatus.builder()
.status(Status.CANCEL).pushHandler(Handler.CANCEL).pushPostHandler(PostHandler.CANCEL)
.build()))
.isInit(false).isFinal(false).isPositive(true).isCouldUpdateMaterial(true)
.materialGroupUpdateRangeList(Arrays.asList("upload", "archive"))
.build(),
StatemachineNode.builder()
.status(Status.UPLOADING)
.desc("上传中")
.nextStatus(Arrays.asList(
NextStatus.builder()
.status(Status.ALREADY_UPLOAD).pushHandler(Handler.ALREADY_UPLOAD).pushPostHandler(PostHandler.ALREADY_UPLOAD)
.build(),
NextStatus.builder()
.status(Status.CANCEL).pushHandler(Handler.CANCEL).pushPostHandler(PostHandler.CANCEL)
.build()))
.isInit(false).isFinal(false).isPositive(true).isCouldUpdateMaterial(true)
.materialGroupUpdateRangeList(Arrays.asList("archive"))
.build(),
StatemachineNode.builder()
.status(Status.ALREADY_UPLOAD)
.desc("已上传")
.nextStatus(Arrays.asList(
NextStatus.builder()
.status(Status.FINISH).pushHandler(Handler.FINISH).pushPostHandler(PostHandler.FINISH)
.build(),
NextStatus.builder()
.status(Status.FAIL).pushHandler(Handler.FAIL).pushPostHandler(PostHandler.FAIL)
.build(),
NextStatus.builder()
.status(Status.CANCEL).pushHandler(Handler.CANCEL).pushPostHandler(PostHandler.CANCEL)
.build()))
.isInit(false).isFinal(false).isPositive(true).isCouldUpdateMaterial(false)
.build(),
StatemachineNode.builder()
.status(Status.FINISH)
.desc("已完成")
.isInit(false).isFinal(true).isPositive(true).isCouldUpdateMaterial(false)
.build(),
StatemachineNode.builder()
.status(Status.FAIL)
.desc("失败")
.isInit(false).isFinal(true).isPositive(false).isCouldUpdateMaterial(false)
.build(),
StatemachineNode.builder()
.status(Status.CANCEL)
.desc("取消")
.isInit(false).isFinal(true).isPositive(null).isCouldUpdateMaterial(false)
.build(),
StatemachineNode.builder()
.status(Status.ENCODE_FAIL)
.desc("压制失败")
.isInit(false).isFinal(true).isPositive(false).isCouldUpdateMaterial(false)
.build()
);
return statemachineNodes;
}
}
视频生产任务流程示意
当生产人员发起视频生产任务后,由生产系统进行任务托管并完成后续流程,整个过程可以主要概括为:
1、首先读取原片地址在媒资基础服务中进行压制,将原片五花八门格式的视频产出成mkv,视频生产任务推进到待压制完成;
2、接收到压制完成的视频地址后,通过媒资基础服务上传到视频云,视频生产任务推进到上传中;
3、接收到上传完成后得到的视频引用信息,使用此信息在视频库服务进行视频物料入库,此期间由视频库服务完成视频内部投稿和稿件转码完成事件的监听工作,视频生产任务推进到已上传;
4、接收到视频库服务落库完成的消息后,视频生产任务推进到完成;
5、视频生产任务完成后自动触发后续其他类型任务,例如视频质量分析任务、视频清晰度增强任务等。
新增任务流程研发模式
研发工作量主要体现在对新业务流程的知识梳理及对外围系统的接入与联调
内容生产的分阶段实现
通过分阶段实现的方式完成系统迭代。
第一二阶段完成了基础架构的搭建,完成了OGV核心业务流程的建设和增强,解决OGV内容生产链路的核心痛点并提效。
第三阶段开始通过技术为业务赋能,通过对基础物料跑批完善、对业务场景的深入思考结合AI能力 以及后续和其他产研团队的共同合作落地了高级物料的产出,诸如智能图片生产、高能点生产。
第四阶段在基于物料信息有沉淀的基础上为其他业务场景赋能,如:智能拼图为大会员商业广告带来降本增效。
当前业务架构
由原先粗放式的工作方式转变为工单排期,生产执行,物料落库的工作模型。
由版权或其他业务方委托需求到生产系统,再以任务工单的形式派发到具体生产人员, 最终产出各种物料进入到各个领域的库中。
生产执行
实际生产执行层面 提供任务的批量发起输入编辑能力,提交后全程由生产系统完成。
生产人员不再需要关注中间过程,只需关注专门的企微应用或邮件通知信息进行后续处理, 极大解放人员精力,使得可以去完成其他更有价值的工作。
三、总结与展望
根据前期收集到的业务知识基于业务特点从0到1的搭建了OGV内容生产系统完成了OGV内容生产工业化,最重要的是对原先仅存留在线下分散的业务流程知识实现了系统固化,极大的提高了整个内容供应链的协作效率和生产效率以及后续的业务知识传承,对于生产内容和过程数据也有了一定的沉淀,为生产策略制定和生产效率优化提供了数据支持。但还是有许多问题可以思考和深化,例如:已经对现有业务知识的常规情况下媒资内容物料生产做到了工业化生产,但整个链路中是不是还有细节可以挖掘?已经固化的业务场景流程对于其他业务场景是不是也可以覆盖进来,例如大会员商广图片的拼接,课堂等的物料生产业务场景是不是也可以?跟上下游衔接的自动化程度是不是可以更进一步?能进一步那么业务效率也能更进一步,这些都是需要思考和探索的地方,难的不是实现而是发现以及发现后的资源获取与说服,如何打通这些环节也是需要努力的地方。
参考文档
[1] 领域驱动设计问题域分析-以bilibili OGV业务为例 https://mp.weixin.qq.com/s/sEN57LXO4Dl6gEZPbXuftQ
-End-
作者丨昭勇
开发者问答
关于物料生产场景大家觉得是中台化通用化优先还是主业务场景适配优先?欢迎在留言区告诉我们。转发并留言,小编将选取1则最有价值的评论,送出bilibili Goods 播放器织带连帽卫衣一件(见下图)。7月19日中午12点开奖。如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路