直播介绍
1、项目需求
硅谷课堂会定期推出直播课程,方便学员与名师之间的交流互动,在直播间老师可以推荐点播课程(类似直播带货),学员可以点赞交流,购买推荐的点播课程。
2、了解直播
一个完整直播实现流程:
1.采集、2.滤镜处理、3.编码、4.推流、5.CDN分发、6.拉流、7.解码、8.播放、9.聊天互动。
2.1、通用直播模型
- 首先是主播方,它是产生视频流的源头,由一系列流程组成:第一,通过一定的设备来采集数据;第二,将采集的这些视频进行一系列的处理,比如水印、美颜和特效滤镜等处理;第三,将处理后的结果视频编码压缩成可观看可传输的视频流;第四,分发推流,即将压缩后的视频流通过网络通道传输出去。
- 其次是播放端,播放端功能有两个层面,第一个层面是关键性的需求;另一层面是业务层面的。先看第一个层面,它涉及到一些非常关键的指标,比如秒开,在很多场景当中都有这样的要求,然后是对于一些重要内容的版权保护。为了达到更好的效果,我们还需要配合服务端做智能解析,这在某些场景下也是关键性需求。再来看第二个层面也即业务层面的功能,对于一个社交直播产品来说,在播放端,观众希望能够实时的看到主播端推过来的视频流,并且和主播以及其他观众产生一定的互动,因此它可能包含一些像点赞、聊天和弹幕这样的功能,以及礼物这样更高级的道具。
- 我们知道,内容产生方和消费方一般都不是一一对应的。对于一个直播产品来讲,最直观的体现就是一个主播可能会有很多粉丝。因此,我们不能直接让主播端和所有播放端进行点对点通信,这在技术上是做不到或者很有难度。主播方播出的视频到达播放端之前,需要经过一系列的中间环节,也就是我们这里讲的直播服务器端。
- 直播服务器端提供的最核心功能是收集主播端的视频推流,并将其放大后推送给所有观众端。除了这个核心功能,还有很多运营级别的诉求,比如鉴权认证,视频连线和实时转码,自动鉴黄,多屏合一,以及云端录制存储等功能。另外,对于一个主播端推出的视频流,中间需要经过一些环节才能到达播放端,因此对中间环节的质量进行监控,以及根据这些监控来进行智能调度,也是非常重要的诉求。
- 实际上无论是主播端还是播放端,他们的诉求都不会仅仅是拍摄视频和播放视频这么简单。在这个核心诉求被满足之后,还有很多关键诉求需要被满足。比如,对于一个消费级的直播产品来说,除了这三大模块之外,还需要实现一个业务服务端来进行推流和播放控制,以及所有用户状态的维持。如此,就构成了一个消费级可用的直播产品。
2.2、如何快速开发完整直播
2.2.1、利用第三方SDK开发
- 七牛云:七牛直播云是专为直播平台打造的全球化直播流服务和一站式实现SDK端到端直播场景的企业级直播云服务平台.
☞ 熊猫TV,龙珠TV等直播平台都是用的七牛云 - 网易视频云:基于专业的跨平台视频编解码技术和大规模视频内容分发网络,提供稳定流畅、低延时、高并发的实时音视频服务,可将视频直播无缝对接到自身App.
- 阿里云视频直播解决方案
☞ 直播推流 SDK(iOS/Android)
☞ 直播播放器 SDK(iOS/Android) - 欢拓云直播平台:欢拓是一家以直播技术为核心的网络平台,旨在帮助人们通过网络也能实现真实互动通讯。
2.2.2、第三方SDK好处
- 降低成本
☞ 使用好的第三方企业服务,将不用再花费大量的人力物力去研发 - 提升效率
☞ 第三方服务的专注与代码集成所带来的方便,所花费的时间可能仅仅是1-2个小时,节约近99%的时间,足够换取更多的时间去和竞争对手斗智斗勇,增加更大的成功可能性 - 降低风险
☞ 借助专业的第三方服务,由于它的快速、专业、稳定等特点,能够极大地加强产品的竞争能力(优质服务、研发速度等),缩短试错时间,必将是创业中保命的手段之一 - 专业的事,找专业的人来做
☞ 第三方服务最少是10-20人的团队专注地解决同一个问题,做同一件事情。
3、欢拓云直播
根据上面的综合对比和调研,我们最终选择了“欢拓与直播平台”,它为我们提供了完整的可以直接使用的示例代码,方便我们开发对接。
欢拓是一家以直播技术为核心的网络平台,旨在帮助人们通过网络也能实现真实互动通讯。从2010年开始,欢拓就专注于音频、视频的采样、编码、后处理及智能传输研究,并于2013年底正式推出了针对企业/开发者的直播云服务系统,帮助开发者轻松实现真人互动。该系统适用场景包括在线教育、游戏语音、娱乐互动、远程会议(PC、移动均可)等等。针对应用场景,采用先进技术解决方案和产品形态,让客户和客户的用户满意!
接口文档地址:http://open.talk-fun.com/docs/getstartV2/document.html
直播对接
1、直播体验
1.1、开通账号
通过官网:https://www.talk-fun.com/,联系客户或400电话开通账号,开通**“生活直播”**权限。开通后注意使用有效期,一般一周左右,可以再次申请延期。
说明:官网免费试用,功能有限制,不建议使用
1.2、创建直播
1、在直播管理创建直播
2、创建直播,选择主播模式
3、配置直播,可以自行查看
1.3、开始直播
1、在直播列表,点击“直播入口”
主播端下载“云直播客户端”,“频道id与密码”为直播客户端的登录账号;
下面还有管理员,主播进行直播时,助教可以在聊天时与观众互动。
2、电脑端安装后如图:
3、使用“频道id与密码”登录
4、点击“开始直播”,打开摄像头即可开始直播。
1.4、用户观看
1、在直播列表,点击“直播入口”
2、在观众一栏点击进入,即可在网页端观看直播。
1.5、体验总结
上面的体验完全能够满足我们业务的需要,硅谷课堂的需求是定期推出直播课程,方便学员与名师之间的交流互动,在直播间老师可以推荐点播课程(类似直播带货),学员可以点赞交流,购买推荐的点播课程。
直播平台只是做了直播相关的业务,不能与我们的业务进行衔接,我们期望是在硅谷课堂的管理后台管理直播相关的业务,那么怎么做呢?对接直播业务接口,直播平台有对应的直播接口,我们直接对接即可。
后台系统—直播管理
上面我们已经开通了“生活类直播”。
1、获取openId与openToken
登录进入开放后台,后台首页即可获取openId与openToken
2、对接说明
1、使用HTTP协议进行信息交互,字符编码统一采用UTF-8
2、除非特殊说明,接口地址统一为:https://api.talk-fun.com/portal.php
3、除非特殊说明,同时支持GET和POST两种参数传递方式
4、除非特殊说明,返回信息支持JSON格式
5、除了sign外,其余所有请求参数值都需要进行URL编码
6、参数表中,类型一栏声明的定义为:int 代表整数类型;string 代表字符串类型,如果后面有括号,括号中的数字代表该参数的最大长度;array/object表示数组类型
7、openID、openToken参数的获取见对接流程说明
3、了解接口文档
接口文档地址:https://open.talk-fun.com/docs/getstartV2/api/live_dir.html
3.1、了解接口文档
根据接口文档,了解我们需要对接哪些接口
(1)添加直播
api名称:course.add
,SDK对应方法:courseAdd
添加直播是一定需要的
(2)更新直播信息
api名称:course.update
,SDK对应方法courseUpdate
(3)删除直播信息
api名称:course.delete
,SDK对应方法:courseDelete
(4)修改生活直播相关配置
api名称:course.updateLifeConfig
,SDK对应方法:updateLifeConfig
设置功能很多,但是我们只需要几个即可,这个接口我们需要做如下设置:
1、界面模式:pageViewMode 界面模式 1全屏模式 0二分屏 2课件模式
2、观看人数开关:number 观看人数开关;number.enable 是否开启 观看人数 0否 1是;示例:
3、商城开关(直播推荐课程):goodsListEdit 商品列表编辑,状态goodsListEdit.status 0覆盖,1追加,不传默认为0;示例:{“status”:1};
直播设置最终效果:
(5)按照课程ID获取访客列表
改接口在:"访客/管理员列表"下面
通过该接口统计课程观看人数信息
直播访客api名称:course.visitor.list
,SDK对应方法:courseVisitorList
3.2、下载SDK
直播平台为我们准备了SDK,我们直接使用
下载地址:https://open.talk-fun.com/docs/getstartV2/api/introduce/sdkdownload.html
已下载:当前目录/MTCloud-java-sdk-1.6.zip
5、搭建service_live模块
5.1、创建service_live模块
5.2、添加依赖
添加直播SDK需要的依赖
<!-- 直播 -->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
5.3、集成代码
解压MTCloud-java-sdk-1.6.zip,复制MTCloud-java-sdk-1.6\MTCloud_java\src\com\mtcloud\sdk下面的java文件到com.atguigu.ggkt.live.mtcloud包下,如图
5.4、更改配置
更改MTCloud类配置
说明:
1、更改openID与openToken
2、该类官方已经做了接口集成,我们可以直接使用。
public class MTCloud {
/**
* 合作方ID: 合作方在欢拓平台的唯一ID
*/
public String openID = "37013";
/**
* 合作方秘钥: 合作方ID对应的参数加密秘钥
*/
public String openToken = "5cfa64c1be5f479aea8296bb4e2c37d3";
...
}
5.5、创建配置文件和启动类
(1)application.properties
# 服务端口
server.port=8306
# 服务名
spring.application.name=service-live
# 环境设置:dev、test、prod
spring.profiles.active=dev
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/glkt_live?characterEncoding=utf-8&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath:com/atguigu/ggkt/live/mapper/xml/*.xml
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
mtcloud.openId=43873
mtcloud.openToken=1f3681df876eb31474be8c479b9f1ffe
(2)启动类
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.atguigu")
@ComponentScan(basePackages = "com.atguigu")
@MapperScan("com.atguigu.ggkt.live.mapper")
public class ServiceLiveApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceLiveApplication.class, args);
}
}
5.6、生成相关代码
6、功能实现-直播课程列表接口
根据直播平台与我们自身业务设计直播相关的业务表,如:glkt_live
6.1、LiveCourseController类
@RestController
@RequestMapping(value="/admin/live/liveCourse")
public class LiveCourseController {
@Autowired
private LiveCourseService liveCourseService;
@Autowired
private LiveCourseAccountService liveCourseAccountService;
@ApiOperation(value = "获取分页列表")
@GetMapping("{page}/{limit}")
public Result index(
@ApiParam(name = "page", value = "当前页码", required = true)
@PathVariable Long page,
@ApiParam(name = "limit", value = "每页记录数", required = true)
@PathVariable Long limit) {
Page<LiveCourse> pageParam = new Page<>(page, limit);
IPage<LiveCourse> pageModel = liveCourseService.selectPage(pageParam);
return Result.ok(pageModel);
}
}
6.2、LiveCourseService接口
public interface LiveCourseService extends IService<LiveCourse> {
//直播课程分页查询
IPage<LiveCourse> selectPage(Page<LiveCourse> pageParam);
}
6.3、service_vod模块创建接口
(1)获取讲师信息
@ApiOperation("根据id查询")
@GetMapping("inner/getTeacher/{id}")
public Teacher getTeacherLive(@PathVariable Long id) {
Teacher teacher = teacherService.getById(id);
return teacher;
}
(2)service_course_client定义接口
@GetMapping("/admin/vod/teacher/inner/getTeacher/{id}")
Teacher getTeacherLive(@PathVariable Long id);
6.4、service_live引入依赖
<dependency>
<groupId>com.atguigu</groupId>
<artifactId>service_course_client</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
6.5、LiveCourseServiceImpl实现
@Service
public class LiveCourseServiceImpl extends ServiceImpl<LiveCourseMapper, LiveCourse> implements LiveCourseService {
@Autowired
private CourseFeignClient courseFeignClient;
//直播课程分页查询
@Override
public IPage<LiveCourse> selectPage(Page<LiveCourse> pageParam) {
IPage<LiveCourse> page = baseMapper.selectPage(pageParam, null);
List<LiveCourse> liveCourseList = page.getRecords();
for(LiveCourse liveCourse : liveCourseList) {
Teacher teacher = courseFeignClient.getTeacherLive(liveCourse.getTeacherId());
liveCourse.getParam().put("teacherName", teacher.getName());
liveCourse.getParam().put("teacherLevel", teacher.getLevel());
}
return page;
}
}
7、功能实现-直播课程添加接口
7.1、添加工具类
(1)MTCloudAccountConfig类
@Data
@Component
@ConfigurationProperties(prefix = "mtcloud")
public class MTCloudAccountConfig {
private String openId;
private String openToken;
}
(2)MTCloudConfig类
@Component
public class MTCloudConfig {
@Autowired
private MTCloudAccountConfig mtCloudAccountConfig;
@Bean
public MTCloud mtCloudClient(){
return new MTCloud(mtCloudAccountConfig.getOpenId(), mtCloudAccountConfig.getOpenToken());
}
}
7.2、LiveCourseController类
@ApiOperation(value = "新增")
@PostMapping("save")
public Result save(@RequestBody LiveCourseFormVo liveCourseVo) {
liveCourseService.save(liveCourseVo);
return Result.ok(null);
}
7.3、LiveCourseService接口
Boolean save(LiveCourseFormVo liveCourseVo);
7.4、LiveCourseServiceImpl实现
@Resource
private LiveCourseAccountService liveCourseAccountService;
@Resource
private LiveCourseDescriptionService liveCourseDescriptionService;
@Autowired
private CourseFeignClient teacherFeignClient;
@Resource
private MTCloud mtCloudClient;
@SneakyThrows
@Transactional(rollbackFor = {Exception.class})
@Override
public Boolean save(LiveCourseFormVo liveCourseFormVo) {
LiveCourse liveCourse = new LiveCourse();
BeanUtils.copyProperties(liveCourseFormVo, liveCourse);
Teacher teacher = teacherFeignClient.getTeacherLive(liveCourseFormVo.getTeacherId());
HashMap<Object, Object> options = new HashMap<>();
options.put("scenes", 2);//直播类型。1: 教育直播,2: 生活直播。默认 1,说明:根据平台开通的直播类型填写
options.put("password", liveCourseFormVo.getPassword());
String res = mtCloudClient.courseAdd(liveCourse.getCourseName(), teacher.getId().toString(), new DateTime(liveCourse.getStartTime()).toString("yyyy-MM-dd HH:mm:ss"), new DateTime(liveCourse.getEndTime()).toString("yyyy-MM-dd HH:mm:ss"), teacher.getName(), teacher.getIntro(), options);
System.out.println("return:: "+res);
CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
if(Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS) {
JSONObject object = commonResult.getData();
liveCourse.setCourseId(object.getLong("course_id"));
baseMapper.insert(liveCourse);
//保存课程详情信息
LiveCourseDescription liveCourseDescription = new LiveCourseDescription();
liveCourseDescription.setDescription(liveCourseFormVo.getDescription());
liveCourseDescription.setLiveCourseId(liveCourse.getId());
liveCourseDescriptionService.save(liveCourseDescription);
//保存课程账号信息
LiveCourseAccount liveCourseAccount = new LiveCourseAccount();
liveCourseAccount.setLiveCourseId(liveCourse.getId());
liveCourseAccount.setZhuboAccount(object.getString("bid"));
liveCourseAccount.setZhuboPassword(liveCourseFormVo.getPassword());
liveCourseAccount.setAdminKey(object.getString("admin_key"));
liveCourseAccount.setUserKey(object.getString("user_key"));
liveCourseAccount.setZhuboKey(object.getString("zhubo_key"));
liveCourseAccountService.save(liveCourseAccount);
} else {
String getmsg = commonResult.getmsg();
throw new GlktException(20001,getmsg);
}
return true;
}
8、功能实现-直播课程删除接口
8.1、LiveCourseController类
@ApiOperation(value = "删除")
@DeleteMapping("remove/{id}")
public Result remove(@PathVariable Long id) {
liveCourseService.removeLive(id);
return Result.ok(null);
}
8.2、LiveCourseService接口
//删除直播课程
void removeLive(Long id);
8.3、LiveCourseServiceImpl实现
//删除直播课程
@Override
public void removeLive(Long id) {
//根据id查询直播课程信息
LiveCourse liveCourse = baseMapper.selectById(id);
if(liveCourse != null) {
//获取直播courseid
Long courseId = liveCourse.getCourseId();
try {
//调用方法删除平台直播课程
mtCloudClient.courseDelete(courseId.toString());
//删除表数据
baseMapper.deleteById(id);
} catch (Exception e) {
e.printStackTrace();
throw new GgktException(20001,"删除直播课程失败");
}
}
}
9、功能实现-直播课程修改接口
9.1、LiveCourseController类
@ApiOperation(value = "获取")
@GetMapping("get/{id}")
public Result<LiveCourse> get(@PathVariable Long id) {
LiveCourse liveCourse = liveCourseService.getById(id);
return Result.ok(liveCourse);
}
@ApiOperation(value = "获取")
@GetMapping("getInfo/{id}")
public Result<LiveCourseFormVo> getInfo(@PathVariable Long id) {
return Result.ok(liveCourseService.getLiveCourseFormVo(id));
}
@ApiOperation(value = "修改")
@PutMapping("update")
public Result updateById(@RequestBody LiveCourseFormVo liveCourseVo) {
liveCourseService.updateById(liveCourseVo);
return Result.ok(null);
}
9.2、LiveCourseService接口
//修改
void updateById(LiveCourseFormVo liveCourseVo);
//获取
LiveCourseFormVo getLiveCourseFormVo(Long id);
9.3、LiveCourseServiceImpl实现
@Resource
private LiveCourseAccountService liveCourseAccountService;
@Resource
private LiveCourseDescriptionService liveCourseDescriptionService;
@Autowired
private CourseFeignClient teacherFeignClient;
@Resource
private MTCloud mtCloudClient;
//更新
@Override
public void updateLiveById(LiveCourseFormVo liveCourseFormVo) {
//根据id获取直播课程基本信息
LiveCourse liveCourse = baseMapper.selectById(liveCourseFormVo.getId());
BeanUtils.copyProperties(liveCourseFormVo,liveCourse);
//讲师
Teacher teacher =
teacherFeignClient.getTeacherInfo(liveCourseFormVo.getTeacherId());
// * course_id 课程ID
// * account 发起直播课程的主播账号
// * course_name 课程名称
// * start_time 课程开始时间,格式:2015-01-01 12:00:00
// * end_time 课程结束时间,格式:2015-01-01 13:00:00
// * nickname 主播的昵称
// * accountIntro 主播的简介
// * options 可选参数
HashMap<Object, Object> options = new HashMap<>();
try {
String res = mtCloudClient.courseUpdate(liveCourse.getCourseId().toString(),
teacher.getId().toString(),
liveCourse.getCourseName(),
new DateTime(liveCourse.getStartTime()).toString("yyyy-MM-dd HH:mm:ss"),
new DateTime(liveCourse.getEndTime()).toString("yyyy-MM-dd HH:mm:ss"),
teacher.getName(),
teacher.getIntro(),
options);
//返回结果转换,判断是否成功
CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
if(Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS) {
JSONObject object = commonResult.getData();
//更新直播课程基本信息
liveCourse.setCourseId(object.getLong("course_id"));
baseMapper.updateById(liveCourse);
//直播课程描述信息更新
LiveCourseDescription liveCourseDescription =
liveCourseDescriptionService.getLiveCourseById(liveCourse.getId());
liveCourseDescription.setDescription(liveCourseFormVo.getDescription());
liveCourseDescriptionService.updateById(liveCourseDescription);
} else {
throw new GgktException(20001,"修改直播课程失败");
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public LiveCourseFormVo getLiveCourseFormVo(Long id) {
LiveCourse liveCourse = this.getById(id);
LiveCourseDescription liveCourseDescription = liveCourseDescriptionService.getByLiveCourseId(id);
LiveCourseFormVo liveCourseFormVo = new LiveCourseFormVo();
BeanUtils.copyProperties(liveCourse, liveCourseFormVo);
liveCourseFormVo.setDescription(liveCourseDescription.getDescription());
return liveCourseFormVo;
}
9.4、LiveCourseDescriptionService添加方法
public interface LiveCourseDescriptionService extends IService<LiveCourseDescription> {
LiveCourseDescription getByLiveCourseId(Long liveCourseId);
}
9.5、LiveCourseDescriptionServiceImpl实现方法
@Service
public class LiveCourseDescriptionServiceImpl extends ServiceImpl<LiveCourseDescriptionMapper, LiveCourseDescription> implements LiveCourseDescriptionService {
@Override
public LiveCourseDescription getByLiveCourseId(Long liveCourseId) {
return this.getOne(new LambdaQueryWrapper<LiveCourseDescription>().eq(LiveCourseDescription::getLiveCourseId, liveCourseId));
}
}
10、功能实现-查看账号接口
10.1、LiveCourseController类
@Autowired
private LiveCourseAccountService liveCourseAccountService;
@ApiOperation(value = "获取")
@GetMapping("getLiveCourseAccount/{id}")
public Result<LiveCourseAccount> getLiveCourseAccount(@PathVariable Long id) {
return Result.ok(liveCourseAccountService.getByLiveCourseId(id));
}
10.2、LiveCourseAccountService接口
public interface LiveCourseAccountService extends IService<LiveCourseAccount> {
LiveCourseAccount getByLiveCourseId(Long liveCourseId);
}
10.3、LiveCourseAccountServiceImpl实现
@Service
public class LiveCourseAccountServiceImpl extends ServiceImpl<LiveCourseAccountMapper, LiveCourseAccount> implements LiveCourseAccountService {
@Override
public LiveCourseAccount getByLiveCourseId(Long liveCourseId) {
return baseMapper.selectOne(new LambdaQueryWrapper<LiveCourseAccount>().eq(LiveCourseAccount::getLiveCourseId, liveCourseId));
}
}
11、功能实现-配置和观看记录接口
11.1、查看配置信息
(1)LiveCourseController类
@ApiOperation(value = "获取")
@GetMapping("getCourseConfig/{id}")
public Result getCourseConfig(@PathVariable Long id) {
return Result.ok(liveCourseService.getCourseConfig(id));
}
(2)LiveCourseService添加方法
//获取配置
LiveCourseConfigVo getCourseConfig(Long id);
(3)LiveCourseServiceImpl实现
@Autowired
private LiveCourseConfigService liveCourseConfigService;
@Autowired
private LiveCourseGoodsService liveCourseGoodsService;
@Override
public LiveCourseConfigVo getCourseConfig(Long id) {
LiveCourseConfigVo liveCourseConfigVo = new LiveCourseConfigVo();
LiveCourseConfig liveCourseConfig = liveCourseConfigService.getByLiveCourseId(id);
if(null != liveCourseConfig) {
List<LiveCourseGoods> liveCourseGoodsList = liveCourseGoodsService.findByLiveCourseId(id);
BeanUtils.copyProperties(liveCourseConfig, liveCourseConfigVo);
liveCourseConfigVo.setLiveCourseGoodsList(liveCourseGoodsList);
}
return liveCourseConfigVo;
}
(4)LiveCourseConfigService添加方法
public interface LiveCourseConfigService extends IService<LiveCourseConfig> {
//查看配置信息
LiveCourseConfig getByLiveCourseId(Long id);
}
(5)LiveCourseConfigServiceImpl实现方法
@Service
public class LiveCourseConfigServiceImpl extends ServiceImpl<LiveCourseConfigMapper, LiveCourseConfig> implements LiveCourseConfigService {
//查看配置信息
@Override
public LiveCourseConfig getByLiveCourseId(Long liveCourseId) {
return baseMapper.selectOne(new LambdaQueryWrapper<LiveCourseConfig>().eq(
LiveCourseConfig::getLiveCourseId,
liveCourseId));
}
}
(6)LiveCourseGoodsService添加方法
public interface LiveCourseGoodsService extends IService<LiveCourseGoods> {
//获取课程商品列表
List<LiveCourseGoods> findByLiveCourseId(Long id);
}
(7)LiveCourseGoodsServiceImpl实现方法
@Service
public class LiveCourseGoodsServiceImpl extends ServiceImpl<LiveCourseGoodsMapper, LiveCourseGoods> implements LiveCourseGoodsService {
//获取课程商品列表
@Override
public List<LiveCourseGoods> findByLiveCourseId(Long liveCourseId) {
return baseMapper.selectList(new LambdaQueryWrapper<LiveCourseGoods>()
.eq(LiveCourseGoods::getLiveCourseId, liveCourseId));
}
}
11.2、修改直播配置信息
(1)LiveCourseController添加方法
@ApiOperation(value = "修改配置")
@PutMapping("updateConfig")
public Result updateConfig(@RequestBody LiveCourseConfigVo liveCourseConfigVo) {
liveCourseService.updateConfig(liveCourseConfigVo);
return Result.ok(null);
}
(2)LiveCourseService添加方法
//修改配置
void updateConfig(LiveCourseConfigVo liveCourseConfigVo);
(3)LiveCourseServiceImpl实现方法
@Override
public void updateConfig(LiveCourseConfigVo liveCourseConfigVo) {
LiveCourseConfig liveCourseConfigUpt = new LiveCourseConfig();
BeanUtils.copyProperties(liveCourseConfigVo, liveCourseConfigUpt);
if(null == liveCourseConfigVo.getId()) {
liveCourseConfigService.save(liveCourseConfigUpt);
} else {
liveCourseConfigService.updateById(liveCourseConfigUpt);
}
liveCourseGoodsService.remove(new LambdaQueryWrapper<LiveCourseGoods>().eq(LiveCourseGoods::getLiveCourseId, liveCourseConfigVo.getLiveCourseId()));
if(!CollectionUtils.isEmpty(liveCourseConfigVo.getLiveCourseGoodsList())) {
liveCourseGoodsService.saveBatch(liveCourseConfigVo.getLiveCourseGoodsList());
}
this.updateLifeConfig(liveCourseConfigVo);
}
/**
* 上传直播配置
* @param liveCourseConfigVo
*/
@SneakyThrows
private void updateLifeConfig(LiveCourseConfigVo liveCourseConfigVo) {
LiveCourse liveCourse = this.getById(liveCourseConfigVo.getLiveCourseId());
//参数设置
HashMap<Object,Object> options = new HashMap<Object, Object>();
//界面模式
options.put("pageViewMode", liveCourseConfigVo.getPageViewMode());
//观看人数开关
JSONObject number = new JSONObject();
number.put("enable", liveCourseConfigVo.getNumberEnable());
options.put("number", number.toJSONString());
//观看人数开关
JSONObject store = new JSONObject();
number.put("enable", liveCourseConfigVo.getStoreEnable());
number.put("type", liveCourseConfigVo.getStoreType());
options.put("store", number.toJSONString());
//商城列表
List<LiveCourseGoods> liveCourseGoodsList = liveCourseConfigVo.getLiveCourseGoodsList();
if(!CollectionUtils.isEmpty(liveCourseGoodsList)) {
List<LiveCourseGoodsView> liveCourseGoodsViewList = new ArrayList<>();
for(LiveCourseGoods liveCourseGoods : liveCourseGoodsList) {
LiveCourseGoodsView liveCourseGoodsView = new LiveCourseGoodsView();
BeanUtils.copyProperties(liveCourseGoods, liveCourseGoodsView);
liveCourseGoodsViewList.add(liveCourseGoodsView);
}
JSONObject goodsListEdit = new JSONObject();
goodsListEdit.put("status", "0");
options.put("goodsListEdit ", goodsListEdit.toJSONString());
options.put("goodsList", JSON.toJSONString(liveCourseGoodsViewList));
}
String res = mtCloudClient.courseUpdateLifeConfig(liveCourse.getCourseId().toString(), options);
CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
if(Integer.parseInt(commonResult.getCode()) != MTCloud.CODE_SUCCESS) {
throw new GgktException(20001,"修改配置信息失败");
}
}
11.3、获取最近直播课程
(1)LiveCourseController添加方法
@ApiOperation(value = "获取最近的直播")
@GetMapping("findLatelyList")
public Result findLatelyList() {
return Result.ok(liveCourseService.findLatelyList());
}
(2)LiveCourseService添加方法
//获取最近的直播
List<LiveCourseVo> findLatelyList();
(3)LiveCourseServiceImpl实现方法
@Override
public List<LiveCourseVo> findLatelyList() {
List<LiveCourseVo> liveCourseVoList = baseMapper.findLatelyList();
for(LiveCourseVo liveCourseVo : liveCourseVoList) {
liveCourseVo.setStartTimeString(new DateTime(liveCourseVo.getStartTime()).toString("yyyy年MM月dd HH:mm"));
liveCourseVo.setEndTimeString(new DateTime(liveCourseVo.getEndTime()).toString("HH:mm"));
Long teacherId = liveCourseVo.getTeacherId();
Teacher teacher = teacherFeignClient.getTeacherInfo(teacherId);
liveCourseVo.setTeacher(teacher);
liveCourseVo.setLiveStatus(this.getLiveStatus(liveCourseVo));
}
return liveCourseVoList;
}
/**
* 直播状态 0:未开始 1:直播中 2:直播结束
* @param liveCourse
* @return
*/
private int getLiveStatus(LiveCourse liveCourse) {
// 直播状态 0:未开始 1:直播中 2:直播结束
int liveStatus = 0;
Date curTime = new Date();
if(DateUtil.dateCompare(curTime, liveCourse.getStartTime())) {
liveStatus = 0;
} else if(DateUtil.dateCompare(curTime, liveCourse.getEndTime())) {
liveStatus = 1;
} else {
liveStatus = 2;
}
return liveStatus;
}
(4)LiveCourseMapper添加方法
public interface LiveCourseMapper extends BaseMapper<LiveCourse> {
//获取最近直播
List<LiveCourseVo> findLatelyList();
}
(5)LiveCourseMapper.xml编写sql语句
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.ggkt.live.mapper.LiveCourseMapper">
<resultMap id="liveCourseMap" type="com.atguigu.ggkt.vo.live.LiveCourseVo" autoMapping="true">
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
id,course_id,course_name,start_time,end_time,teacher_id,cover,create_time,update_time,is_deleted
</sql>
<select id="findLatelyList" resultMap="liveCourseMap">
select <include refid="columns" />
from live_course
where date(start_time) >= curdate()
order by id asc
limit 5
</select>
</mapper>
前台系统—用户集成
12. 直播课程管理前端整合
12.1、service_vod添加方法
(1)CourseController添加方法
@GetMapping("findAll")
public Result findAll() {
List<Course> list = courseService.findlist();
return Result.ok(list);
}
(2)CourseService实现方法
@Override
public List<Course> findlist() {
List<Course> list = baseMapper.selectList(null);
list.stream().forEach(item -> {
this.getTeacherAndSubjectName(item);
});
return list;
}
12.2、router->index.js路由
{
path: '/live',
component: Layout,
redirect: '/live/liveCourse/list',
name: 'Live',
meta: {
title: '直播管理',
icon: 'el-icon-bangzhu'
},
alwaysShow: true,
children: [
{
path: 'liveCourse/list',
name: 'liveCourseList',
component: () => import('@/views/live/liveCourse/list'),
meta: { title: '直播列表' }
},
{
path: 'liveCourse/config/:id',
name: 'liveCourseConfig',
component: () => import('@/views/live/liveCourse/config'),
meta: { title: '直播配置' },
hidden: true
},
{
path: 'liveVisitor/list/:id',
name: 'liveVisitor',
component: () => import('@/views/live/liveVisitor/list'),
meta: { title: '观看记录' },
hidden: true
}
]
},
12.3、定义调用接口
import request from '@/utils/request'
const api_name = '/admin/live/liveCourse'
export default {
getPageList(page, limit) {
return request({
url: `${api_name}/${page}/${limit}`,
method: 'get'
})
},
findLatelyList() {
return request({
url: `${api_name}/findLatelyList`,
method: 'get'
})
},
getById(id) {
return request({
url: `${api_name}/getInfo/${id}`,
method: 'get'
})
},
getLiveCourseAccount(id) {
return request({
url: `${api_name}/getLiveCourseAccount/${id}`,
method: 'get'
})
},
save(liveCourse) {
return request({
url: `${api_name}/save`,
method: 'post',
data: liveCourse
})
},
updateById(liveCourse) {
return request({
url: `${api_name}/update`,
method: 'put',
data: liveCourse
})
},
removeById(id) {
return request({
url: `${api_name}/remove/${id}`,
method: 'delete'
})
},
removeRows(idList) {
return request({
url: `${api_name}/batchRemove`,
method: 'delete',
data: idList
})
},
getCourseConfig(id) {
return request({
url: `${api_name}/getCourseConfig/${id}`,
method: 'get'
})
},
updateConfig(liveCourseConfigVo) {
return request({
url: `${api_name}/updateConfig`,
method: 'put',
data: liveCourseConfigVo
})
},
}
findAll() {
return request({
url: `${api_name}/findAll`,
method: 'get'
})
},
12.4、创建直播页面
(1)list.vue
<template>
<div class="app-container">
<!-- 工具条 -->
<el-card class="operate-container" shadow="never">
<i class="el-icon-tickets" style="margin-top: 5px"></i>
<span style="margin-top: 5px">数据列表</span>
<el-button class="btn-add" size="mini" @click="add">添 加</el-button>
</el-card>
<!-- banner列表 -->
<el-table
v-loading="listLoading"
:data="list"
stripe
border
style="width: 100%;margin-top: 10px;">
<el-table-column
label="序号"
width="50"
align="center">
<template slot-scope="scope">
{{ (page - 1) * limit + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="封面" width="200" align="center">
<template slot-scope="scope">
<img :src="scope.row.cover" width="100%">
</template>
</el-table-column>
<el-table-column prop="courseName" label="直播名称" />
<el-table-column prop="startTime" label="直播时间">
<template slot-scope="scope">
{{ scope.row.param.startTimeString }}至{{ scope.row.param.endTimeString }}
</template>
</el-table-column>
<el-table-column prop="endTime" label="直播结束时间" />
<el-table-column prop="param.teacherName" label="直播老师" />
<el-table-column label="头衔" width="90">
<template slot-scope="scope">
<el-tag v-if="scope.row.param.teacherLevel === 1" type="success" size="mini">高级讲师</el-tag>
<el-tag v-if="scope.row.param.teacherLevel === 0" size="mini">首席讲师</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" width="200" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="edit(scope.row.id)">修改</el-button>
<el-button type="text" size="mini" @click="removeDataById(scope.row.id)">删除</el-button>
<el-button type="text" size="mini" @click="showAccount(scope.row)">查看账号</el-button>
<router-link :to="'/live/liveCourse/config/'+scope.row.id">
<el-button type="text" size="mini">配置</el-button>
</router-link>
<router-link :to="'/live/liveVisitor/list/'+scope.row.id">
<el-button type="text" size="mini">观看记录</el-button>
</router-link>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
:current-page="page"
:total="total"
:page-size="limit"
:page-sizes="[5, 10, 20, 30, 40, 50, 100]"
style="padding: 30px 0; text-align: center;"
layout="sizes, prev, pager, next, jumper, ->, total, slot"
@current-change="fetchData"
@size-change="changeSize"
/>
<el-dialog title="添加/修改" :visible.sync="dialogVisible" width="60%" >
<el-form ref="flashPromotionForm" label-width="150px" size="small" style="padding-right: 40px;">
<!-- 课程讲师 -->
<el-form-item label="直播讲师">
<el-select
v-model="liveCourse.teacherId"
placeholder="请选择">
<el-option
v-for="teacher in teacherList"
:key="teacher.id"
:label="teacher.name"
:value="teacher.id"/>
</el-select>
</el-form-item>
<el-form-item label="直播讲师登录密码" v-if="liveCourse.id === ''">
<el-input v-model="liveCourse.password"/>
</el-form-item>
<el-form-item label="直播名称">
<el-input v-model="liveCourse.courseName"/>
</el-form-item>
<el-form-item label="直播开始时间">
<el-date-picker
v-model="liveCourse.startTime"
type="datetime"
placeholder="选择开始日期"
value-format="yyyy-MM-dd HH:mm:ss" />
</el-form-item>
<el-form-item label="直播结束时间">
<el-date-picker
v-model="liveCourse.endTime"
type="datetime"
placeholder="选择结束日期"
value-format="yyyy-MM-dd HH:mm:ss" />
</el-form-item>
<el-form-item label="直播封面">
<el-upload
:show-file-list="false"
:on-success="handleCoverSuccess"
:before-upload="beforeCoverUpload"
:on-error="handleCoverError"
:action="BASE_API+'/admin/vod/file/upload?module=cover'"
class="cover-uploader">
<img v-if="liveCourse.cover" :src="liveCourse.cover" width="60%">
<i v-else class="el-icon-plus avatar-uploader-icon"/>
</el-upload>
</el-form-item>
<el-form-item label="直播详情">
<el-input v-model="liveCourse.description" type="textarea" rows="5"/>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false" size="small">取 消</el-button>
<el-button type="primary" @click="saveOrUpdate()" size="small">确 定</el-button>
</span>
</el-dialog>
<el-dialog title="查看账号" :visible.sync="accountDialogVisible" width="60%" >
<el-form ref="accountForm" label-width="150px" size="small" style="padding-right: 40px;">
<div style="margin-left: 40px;">
<h4>主播帮助信息</h4>
<el-row style="height:35px;">
<el-co >
<span class="spd-info">主播登录链接:</span>
<span class="spd-info">https://live.zhibodun.com/live/courseLogin.php?course_id={{ liveCourseAccount.courseId }}&role=admin</span>
</el-co>
</el-row>
<el-row style="height:35px;">
<el-col >
<span class="spd-info">主播登录密码:{{ liveCourseAccount.zhuboKey }}</span>
</el-col>
</el-row>
</div>
<div style="margin-left: 40px;">
<h4>主播客户端账号信息</h4>
<el-row style="height:35px;">
<el-col >
<span class="spd-info">主播登录账户:{{ liveCourseAccount.zhuboAccount }}</span>
</el-col>
</el-row>
<el-row style="height:35px;">
<el-col >
<span class="spd-info">主播登录密码:{{ liveCourseAccount.zhuboPassword }}</span>
</el-col>
</el-row>
</div>
<div style="margin-left: 40px;">
<h4>助教信息</h4>
<el-row style="height:35px;">
<el-co >
<span class="spd-info">助教登录连接:</span>
<span class="spd-info">https://live.zhibodun.com/live/courseLogin.php?course_id={{ liveCourseAccount.courseId }}&role=admin</span>
</el-co>
</el-row>
<el-row style="height:35px;">
<el-col>
<span class="spd-info">主播登录密码:{{ liveCourseAccount.adminKey }}</span>
</el-col>
</el-row>
</div>
<div style="margin-left: 40px;">
<h4>学生观看信息</h4>
<el-row style="height:35px;">
<el-co >
<span class="spd-info">观看连接:</span>
<span class="spd-info">http://glkt-api.atguigu.cn/#/liveInfo/{{ liveCourseAccount.courseId }}</span>
</el-co>
</el-row>
<el-row style="height:35px;">
<el-col>
<span class="spd-info">观看二维码:<img src="@/styles/qrcode.png" width="80px"/></span>
</el-col>
</el-row>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="accountDialogVisible = false" size="small">关 闭</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import api from '@/api/live/liveCourse'
import teacherApi from '@/api/vod/teacher'
const defaultForm = {
id: '',
courseName: '',
startTime: '',
endTime: '',
teacherId: '',
password: '',
description: '',
cover: 'https://cdn.uviewui.com/uview/swiper/1.jpg'
}
export default {
data() {
return {
BASE_API: 'http://localhost:8333',
listLoading: true, // 数据是否正在加载
list: null, // banner列表
total: 0, // 数据库中的总记录数
page: 1, // 默认页码
limit: 10, // 每页记录数
searchObj: {}, // 查询表单对象
teacherList: [], // 讲师列表
dialogVisible: false,
liveCourse: defaultForm,
saveBtnDisabled: false,
accountDialogVisible: false,
liveCourseAccount: {
courseId: ''
}
}
},
// 生命周期函数:内存准备完毕,页面尚未渲染
created() {
console.log('list created......')
this.fetchData()
// 获取讲师列表
this.initTeacherList()
},
// 生命周期函数:内存准备完毕,页面渲染成功
mounted() {
console.log('list mounted......')
},
methods: {
// 当页码发生改变的时候
changeSize(size) {
console.log(size)
this.limit = size
this.fetchData(1)
},
// 加载banner列表数据
fetchData(page = 1) {
console.log('翻页。。。' + page)
// 异步获取远程数据(ajax)
this.page = page
api.getPageList(this.page, this.limit).then(
response => {
this.list = response.data.records
this.total = response.data.total
// 数据加载并绑定成功
this.listLoading = false
}
)
},
// 获取讲师列表
initTeacherList() {
teacherApi.list().then(response => {
this.teacherList = response.data
})
},
// 重置查询表单
resetData() {
console.log('重置查询表单')
this.searchObj = {}
this.fetchData()
},
// 根据id删除数据
removeDataById(id) {
// debugger
this.$confirm('此操作将永久删除该记录, 是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => { // promise
// 点击确定,远程调用ajax
return api.removeById(id)
}).then((response) => {
this.fetchData(this.page)
if (response.code) {
this.$message({
type: 'success',
message: '删除成功!'
})
}
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
// -------------
add(){
this.dialogVisible = true
this.liveCourse = Object.assign({}, defaultForm)
},
edit(id) {
this.dialogVisible = true
this.fetchDataById(id)
},
fetchDataById(id) {
api.getById(id).then(response => {
this.liveCourse = response.data
})
},
saveOrUpdate() {
this.saveBtnDisabled = true // 防止表单重复提交
if (!this.liveCourse.id) {
this.saveData()
} else {
this.updateData()
}
},
// 新增
saveData() {
api.save(this.liveCourse).then(response => {
if (response.code) {
this.$message({
type: 'success',
message: response.message
})
this.dialogVisible = false
this.fetchData(this.page)
}
})
},
// 根据id更新记录
updateData() {
api.updateById(this.liveCourse).then(response => {
if (response.code) {
this.$message({
type: 'success',
message: response.message
})
this.dialogVisible = false
this.fetchData(this.page)
}
})
},
// 根据id查询记录
fetchDataById(id) {
api.getById(id).then(response => {
this.liveCourse = response.data
})
},
showAccount(row) {
this.accountDialogVisible = true
api.getLiveCourseAccount(row.id).then(response => {
this.liveCourseAccount = response.data
this.liveCourseAccount.courseId = row.courseId
})
},
// ------------upload------------
// 上传成功回调
handleCoverSuccess(res, file) {
this.liveCourse.cover = res.data
},
// 上传校验
beforeCoverUpload(file) {
const isJPG = file.type === 'image/jpeg'
const isLt2M = file.size / 1024 / 1024 < 2
if (!isJPG) {
this.$message.error('上传头像图片只能是 JPG 格式!')
}
if (!isLt2M) {
this.$message.error('上传头像图片大小不能超过 2MB!')
}
return isJPG && isLt2M
},
// 错误处理
handleCoverError() {
console.log('error')
this.$message.error('上传失败2')
},
}
}
</script>
<style scoped>
.cover-uploader .avatar-uploader-icon {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
font-size: 28px;
color: #8c939d;
width: 450px;
height: 200px;
line-height: 200px;
text-align: center;
}
.cover-uploader .avatar-uploader-icon:hover {
border-color: #409EFF;
}
.cover-uploader img {
width: 450px;
height: 200px;
display: block;
}
</style>
(2)config.vue
<template>
<div class="app-container">
<el-form label-width="120px" size="small">
<div style="background-color:#E0E0E0;width: 100%;padding: 1px 10px;margin: 10px 0;"><h3>
功能设置
</h3></div>
<el-form-item label="界面模式">
<el-radio-group v-model="liveCourseConfigVo.pageViewMode">
<el-radio :label="1">全屏模式</el-radio>
<el-radio :label="0">二分屏</el-radio>
<el-radio :label="2">课件模式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="观看人数开关">
<el-radio-group v-model="liveCourseConfigVo.numberEnable">
<el-radio :label="1">是</el-radio>
<el-radio :label="0">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="商城开关:">
<el-radio-group v-model="liveCourseConfigVo.storeEnable">
<el-radio :label="1">是</el-radio>
<el-radio :label="0">否</el-radio>
</el-radio-group>
</el-form-item>
<div style="background-color:#E0E0E0;width: 100%;padding: 1px 10px;margin: 10px 0;"><h3>
商品列表
<el-button type="" size="mini" @click="addCourse()">添加</el-button>
</h3></div>
<el-table
v-loading="listLoading"
:data="liveCourseConfigVo.liveCourseGoodsList"
stripe
border
style="width: 100%;margin-top: 10px;">
<el-table-column
label="序号"
width="70"
align="center">
<template slot-scope="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="商品图片" width="120" align="center">
<template slot-scope="scope">
<img :src="scope.row.img" width="80px">
</template>
</el-table-column>
<el-table-column prop="name" label="名称" width="100"/>
<el-table-column prop="price" label="价格" width="100"/>
<el-table-column prop="originalPrice" label="原价"/>
<el-table-column label="操作" width="100" align="center">
<template slot-scope="scope">
<el-button type="text" size="mini" @click="removeCourseById(scope.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="添加课程" :visible.sync="dialogVisible" width="50%">
<el-form :inline="true" label-width="150px" size="small" style="padding-right: 40px;">
<el-table
v-loading="listLoading"
:data="courseList"
stripe
border
style="width: 100%;margin-top: 10px;"
@selection-change="handleSelectionChange">
<el-table-column
type="selection"
width="55" />
<el-table-column
label="序号"
width="70"
align="center">
<template slot-scope="scope">
{{ scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="分类">
<template slot-scope="scope">
{{ scope.row.param.subjectParentTitle }} > {{ scope.row.param.subjectTitle }}
</template>
</el-table-column>
<el-table-column prop="title" label="课程名称" width="150"/>
<el-table-column prop="lessonNum" label="课时" width="100"/>
<el-table-column prop="param.teacherName" label="讲师"/>
</el-table>
<el-form-item style="margin-top: 10px;">
<el-button type="" @click="dialogVisible = false">取消</el-button>
<el-button type="" @click="selectCourse()">保存</el-button>
</el-form-item>
</el-form>
</el-dialog>
<br/><br/>
<el-form-item>
<el-button type="primary" @click="saveOrUpdate">保存</el-button>
<el-button @click="back">返回</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import api from '@/api/live/liveCourse'
import courseApi from '@/api/vod/course'
const defaultForm = {
id: '',
liveCourseId: '',
pageViewMode: 1,
numberEnable: 1,
storeEnable: 1,
storeType: 1,
liveCourseGoodsList: []
}
export default {
data() {
return {
listLoading: true, // 数据是否正在加载
liveCourseConfigVo: defaultForm,
saveBtnDisabled: false,
dialogVisible: false,
courseList: [],
multipleSelection: [] // 批量选择中选择的记录列表
}
},
// 监听器
watch: {
$route(to, from) {
console.log('路由变化......')
console.log(to)
console.log(from)
this.init()
}
},
// 生命周期方法(在路由切换,组件不变的情况下不会被调用)
created() {
console.log('form created ......')
this.init()
},
methods: {
// 表单初始化
init() {
this.liveCourseConfigVo.liveCourseId = this.$route.params.id
this.fetchDataById(this.liveCourseConfigVo.liveCourseId)
this.fetchCourseList()
},
back() {
this.$router.push({ path: '/live/liveCourse/list' })
},
// 根据id查询记录
fetchDataById(id) {
api.getCourseConfig(id).then(response => {
if(null !== response.data.id) {
this.liveCourseConfigVo = response.data
}
this.listLoading = false
})
},
fetchCourseList() {
courseApi.findAll().then(response => {
//debugger
this.courseList = response.data
})
},
handleSelectionChange(selection) {
console.log(selection)
this.multipleSelection = selection
},
addCourse() {
this.dialogVisible = true
},
selectCourse() {
if (this.multipleSelection.length === 0) {
this.$message({
type: 'warning',
message: '请选择对应课程!'
})
return
}
var list = []
this.multipleSelection.forEach(item => {
var obj = {
liveCourseId: this.liveCourseConfigVo.liveCourseId,
goodsId: item.id,
name: item.title,
img: item.cover,
price: item.price,
originalPrice: item.price,
tab: '1',
url: 'http://glkt-api.atguigu.cn/#/courseInfo/'+item.id,
putaway: '1',
pay: '1',
qrcode: ''
}
list.push(obj)
})
this.liveCourseConfigVo.liveCourseGoodsList = list
this.dialogVisible = false
},
removeCourseById(index) {
this.liveCourseConfigVo.liveCourseGoodsList.splice(index, 1)
},
saveOrUpdate() {
api.updateConfig(this.liveCourseConfigVo).then(response => {
this.$message({
type: 'success',
message: response.message
})
this.$router.push({ path: '/live/liveCourse/list' })
})
}
}
}
</script>
<style scoped>
.littleMarginTop {
margin-top: 10px;
}
.paramInput {
width: 250px;
}
.paramInputLabel {
display: inline-block;
width: 100px;
text-align: right;
padding-right: 10px
}
.cardBg {
background: #F8F9FC;
}
</style>
13、用户观看端集成
接口文档:https://open.talk-fun.com/docs/js/index.html
13.1、获取用户access_token
用户要观看直播,必须获取对应的用户access_token,通过access_token 获取观看的直播课程;
接口参数:直播id,用户id
(1)创建LiveCourseApiController
@RestController
@RequestMapping("api/live/liveCourse")
public class LiveCourseApiController {
@Resource
private LiveCourseService liveCourseService;
@ApiOperation(value = "获取用户access_token")
@GetMapping("getPlayAuth/{id}")
public Result<JSONObject> getPlayAuth(@PathVariable Long id) {
JSONObject object = liveCourseService.getPlayAuth(id, AuthContextHolder.getUserId());
return Result.ok(object);
}
}
(2)LiveCourseService添加方法
JSONObject getPlayAuth(Long id, Long userId);
(3)LiveCourseServiceImpl实现方法
@SneakyThrows
@Override
public JSONObject getPlayAuth(Long id, Long userId) {
LiveCourse liveCourse = this.getById(id);
UserInfo userInfo = userInfoFeignClient.getById(userId);
HashMap<Object,Object> options = new HashMap<Object, Object>();
String res = mtCloudClient.courseAccess(liveCourse.getCourseId().toString(), userId.toString(), userInfo.getNickName(), MTCloud.ROLE_USER, 80*80*80, options);
CommonResult<JSONObject> commonResult = JSON.parseObject(res, CommonResult.class);
if(Integer.parseInt(commonResult.getCode()) == MTCloud.CODE_SUCCESS) {
JSONObject object = commonResult.getData();
System.out.println("access::"+object.getString("access_token"));
return object;
} else {
throw new GgktException(20001,"获取失败");
}
}
13.2、下载前端SDK
下载地址:https://open.talk-fun.com/docs/js/download.html
13.3、使用快捷模板
下载模板,修改token获取方式
var url = window.location.search
var token = url.split("=")[1]
13.4、与前端项目结合
(1)创建直播播放页面 live.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
<title>TalkFun Live QuickStart v2.2</title>
<style type="text/css">
* {
margin: 0;
padding: 0;
list-style-type: none;
font-family: "Microsoft YaHei", "STHeiti";
}
.flash-wran {
display: none;
position: absolute;
top: 0;
width: 100%;
padding: 5px 0;
text-align: center;
background: #fff6a2;
border: 1px solid #ffd913;
}
.flash-wran a {
color: red;
}
.wrapper {
/*display: flex;*/
padding: 10px;
}
#cameraPlayer,
#pptPlayer {
height: auto;
flex: 1;
text-align: center;
font-size: 12px;
overflow: hidden;
}
#pptPlayer {
height: 300px;
width: 100%;
}
#modPptPlayer,
#modCameraPlayer {
margin-top: 10px;
border: 1px solid #c7c7c7;
}
.chat-wrap {
padding: 5px;
margin: 10px;
border: 2px solid #cccccc;
background: #f1f1f1
}
.mod-chat-list {
margin: 20px 0;
border: 1px solid #CCCCCC;
min-height: 100px;
font-size: 12px;
background: #dedede;
padding: 5px;
max-height: 200px;
overflow-y: scroll;
}
.mod-chat-list li {
padding: 2px 0;
margin-bottom: 5px;
border-bottom: 1px dotted #CCCCCC;
}
input {
display: inline-block;
width: 200px;
padding: 5px;
}
button {
display: inline-block;
padding: 5px;
margin-left: 5px;
}
#toast {
padding: 20px;
position: fixed;
z-index: 100;
display: none;
background: rgba(212, 28, 28, 0.8);
left: 50%;
top: 30%;
border-radius: 50em;
font-size: 14px;
color: #FFFFFF;
box-shadow: 0 0 6px 0px #bb2d2d;
}
#talkfun-video-wrap, #talkfun-camera-wrap {
position: relative;
background: #000;
}
</style>
<!-- #SDK版本 -->
<!-- #获取最新版本 ==> http://open.talk-fun.com/docs/js/changelog/live.html -->
<script type="text/javascript" src="https://static-1.talk-fun.com/open/TalkFun_SDK_Pack/v6.0/TalkFunWebSDK-6.2-2.min.js"></script>
</head>
<body>
<!-- #toast -->
<div id="toast"></div>
<!-- #wrap -->
<div class="wrapper">
<!-- #画板播放器 -->
<div id="pptPlayer">
<p id="loaddoc">播放器 Loading...</p>
</div>
<!-- #摄像头模式 -->
<div id="cameraPlayer">
<p id="loadcam">摄像头 Loading...</p>
</div>
<!-- #桌面分享|视频插播模式 -->
<div id="videoPlayer">
<p id="loadplayer">视频播放器 Loading...</p>
</div>
</div>
<!-- #chat -->
<div class="chat-wrap">
<h4>聊天模块</h4>
<ul id="chat-list" class="mod-chat-list"></ul>
<label>
<input id="chatVal" type="text" /><button id="chatSubmit">发送聊天</button>
</label>
</div>
<script>
// [第一步] 如何获取 access_token => http://open.talk-fun.com/docs/getstartV2/access_token.html
// [第二步] 根据Api文档方法 监听 / 调用方法 JS Api => http://open.talk-fun.com/docs/js/sdk.js.getstart.html
var url = window.location.search
var token = url.split("=")[1]
// 更多配置项 => https://open.talk-fun.com/docs/js/sdk.js.getstart.html?h=%E9%85%8D%E7%BD%AE%E9%A1%B9
var HT = new MT.SDK.main(token, {
config: {
techOrder: 'FLV' // Safari 浏览器建议设置为 HLS
}
}, function (data) {
console.warn('sdk加载完成', data)
})
// 连接状态
HT.on('connect', function () {
console.log('TalkFun通信 => 连接成功...')
})
// 课件播放器
HT.whiteboardPlayer('pptPlayer', 'docPlayer', function (player) {
console.log('课件播放器 => 初始化成功')
document.querySelector('#loadplayer').innerHTML = '画板模块加载完成'
})
// 视频插播 | 桌面分享
HT.videoPlayer('videoPlayer', 'modVideoplayer', function (player) {
console.log('视频播放器 => 初始化成功')
document.querySelector('#loadplayer').innerHTML = '视频插播加载完成'
})
// 摄像头播放器
HT.camera('cameraPlayer', 'modCameraPlayer', function () {
console.log('摄像头播放器 => 初始化成功')
document.querySelector('#loadcam').innerHTML = '摄像头模块加载完成'
})
// 接收聊天
var receivedChat = function (chat) {
var tpl = chat.nickname + ': ' + chat.msg
var chatItem = document.createElement('li')
chatItem.innerHTML = tpl
chatItem.className = 'chat-' + chat.xid
document.querySelector('#chat-list').appendChild(chatItem)
}
// 接收聊天信息
HT.on('chat:send', function (chat) {
receivedChat(chat)
})
// 发送聊天信息
document.querySelector('#chatSubmit').addEventListener('click', function () {
var chatIpt = document.querySelector('#chatVal')
var chatValue = chatIpt.value
HT.emit('chat:send', { msg: chatValue }, function (res) {
// 发送成功
if (Number(res.code) === 0) {
receivedChat(res.data)
chatIpt.value = ''
}
// 发送失败
else {
console.warn(res.msg)
}
})
}, false)
// Flash插件异常
HT.on('flash:load:error', function (obj) {
if (!obj.flash) {
document.querySelector('#flashTip').style.display = 'block'
}
})
// 课程错误信息
HT.on('live:course:access:error', function (res) {
console.error('错误信息 ==>', res)
})
// 课程错误信息
HT.on('system:room:error', function (res) {
var toast = document.querySelector('#toast')
if (typeof res === 'string') {
toast.innerHTML = res.msg
}
else if (res.msg) {
toast.innerHTML = res.msg
}
toast.style.display = 'block'
var _left = toast.clientWidth / 2
toast.style.marginLeft = -_left + 'px'
})
</script>
</body>
</html>
观众在直播详情页面点击观看,获取通过接口获取access_token,然后带上access_token参数跳转到直播观看页面即可,关键代码:
liveInfo.vue
play() {
api.getPlayAuth(this.liveCourseId).then(response => {
console.log(response.data);
window.location = './live.html?token='+response.data.access_token;
this.finished = true;
});
},
http://localhost:8080/live.html为直播观看访问方式
本文由 liyunfei 创作,采用 知识共享署名4.0
国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: Jul 5,2022