流程分析
1. 选取合适的厂商服务,比如腾讯、阿里的直播服务进行搭建。
2. 配置好厂商服务后,根据厂商的服务编写后端代码,生成推流地址和播放地址
3. 前端或app创建房间请求后端,后端根据房间生成推流地址,将推流返给前端,前端或app开始推流,后端生成数据入库,比如播放地址
4. 播放列表,前端根据后端的播放地址播放直播。
云服务搭建(以腾讯云为例)
步骤:
1) 注册 腾讯云账号,并完成 实名认证。
2) 进入 腾讯云直播服务开通页,勾选同意《腾讯云服务协议》,并单击【申请开通】即可开通云直播服务,购买相关套餐
3) 需要自己的域名来(推流和播放)域名可以以CNAME的形式来配置,默认提供推流域名,自己需要设置播放域名(域名需要通过备案)https://cloud.tencent.com/document/product/267/20381
选择云直播控制台的 【域名管理】>【添加域名】添加您已备案后的推流域名和播放域名。
将域名解析地址 CNAME 到云直播控制台的域名列表中对应域名的 CNAME 地址。以 DNS 服务商为腾讯云为例,添加 CNAME 记录操作步骤。
4) 获取推流地址 (需要后端写程序辅助),地址生成器
5) 直播推流 (将生成好的推流地址输入到对应的推流软件中)
6) 获取播放地址
7) 推流成功后,选择【流管理】>【在线流】,查看推流地址状态,单击【测试】在线播放观看。
8) 选择【辅助工具】>【地址生成器】 获取播放地址
后端搭建
防盗链生成工具类:
package com.xyl.live.utils;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
/**
* 直播工具类
*/
public class LiveUtils {
private static final char[] DIGITS_LOWER =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/*
* KEY+ streamName + txTime
*/
public static String getSafeUrl(String key, String streamName, long txTime) {
String input = new StringBuilder().
append(key).
append(streamName).
append(Long.toHexString(txTime).toUpperCase()).toString();
String txSecret = null;
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
txSecret = byteArrayToHexString(
messageDigest.digest(input.getBytes("UTF-8")));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return txSecret == null ? "" :
new StringBuilder().
append("txSecret=").
append(txSecret).
append("&").
append("txTime=").
append(Long.toHexString(txTime).toUpperCase()).
toString();
}
/**
* 字节数组转字符串
* @param data
* @return
*/
private static String byteArrayToHexString(byte[] data) {
char[] out = new char[data.length << 1];
for (int i = 0, j = 0; i >> 4];
out[j++] = DIGITS_LOWER[0x0F & data[i]];
}
return new String(out);
}
/**
* generate uuid
* @return
*/
public static String createUUID(){
return UUID.randomUUID().toString().replace("-","").toLowerCase();
}
}
服务类:
package com.xyl.live.service;
import com.xyl.live.utils.LiveUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class MagicLiveService {
@Value("${tencent.live.pushkey}")
private String pushKey;
@Value("${tencent.live.pushdomain}")
private String pushDomain;
@Value("${tencent.live.broadcastkey}")
private String broadcastKey;
@Value("${tencent.live.broadcastdomain}")
private String broadcastDomain;
@Value("${tencent.live.broadcastexpiretime}")
private Long broadcastExpireTime;
// 过期时间暂时设置为一天
private long pushtime=60*60*24;
//播放过期时间
private long pulltime=60*60*24;
// 获取直播推流地址
public String getPushUrl(String streamName){
return "rtmp://"+pushDomain+"/live/"+streamName+"?"+ LiveUtils.getSafeUrl(pushKey,streamName,pushtime+(System.currentTimeMillis()/1000));
}
// 获取直播拉流播放地址
public String getPullUrl(String streamName){
// return "http://"+broadcastDomain+"/live/"+streamName+".m3u8?"+ LiveUtils.getSafeUrl(pushKey,streamName,pulltime+broadcastExpireTime);
return "http://"+broadcastDomain+"/live/"+streamName+".m3u8?"+ LiveUtils.getSafeUrl(broadcastKey,streamName,pulltime+(System.currentTimeMillis()/1000)-broadcastExpireTime);
}
}
yml 文件配置:
# 推流防盗链
tencent:
live:
pushkey: 12345678
pushdomain: xxxxx.hello.cloud.com
broadcastkey: 123456789012
broadcastdomain: hello.live.cm
broadcastexpiretime: 3600
再编写对应的接口就可以了。
app 端搭建
采用 uni-app 的形式搭建app(参考: https://ext.dcloud.net.cn/plugin?id=226)
<template>
<view class="content">
<view class="butlist">
<view class="buticon" @click="startPusher">
<view class="x_f"></view>
<view :class="begin==true?'givebegin':'give'" >开始</view>
<view class="pulse" v-if="begin"></view>
</view>
<view>
推流地址:<input placeholder="请输入推流地址" v-model="livepushurl"/>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
begin:false,//开始录制
currentWebview:null,
pusher:null,
livepushurl:'rtmp://xxxx.live.aaaa.com/live/abc?txSecret=1a2333f7dfgh49013e34691a7df1a3984af&txTime=5E44DD1C'
}
},
onLoad(res) {
this.getwebview()//获取webview
},
methods: {
/**
* 获取当前显示的webview
*/
getwebview(){
var pages = getCurrentPages();
var page = pages[pages.length - 1];
// #ifdef APP-PLUS
var getcurrentWebview = page.$getAppWebview();
console.log(this.pages)
console.log(this.page)
console.log(JSON.stringify(page.$getAppWebview()))
this.currentWebview=getcurrentWebview;
// #endif
this.plusReady()//创建LivePusher对象
},
/**
* 创建LivePusher对象 即推流对象
*/
plusReady(){
// 创建直播推流控件
this.pusher =new plus.video.LivePusher('pusher',{
url:'',
top:'0',
left:'0px',
width: '100%',
height: uni.getSystemInfoSync().windowHeight-155 + 'px',
position: 'absolute',//static静态布局模式,如果页面存在滚动条则随窗口内容滚动,absolute绝对布局模式,如果页面存在滚动条不随窗口内容滚动; 默认值为"static"
beauty:'1',//美颜 0-off 1-on
whiteness:'0',//0、1、2、3、4、5,0不使用美白,值越大美白程度越大。
aspect:'9:16',
});
this.currentWebview.append(this.pusher);
// 监听状态变化事件
this.pusher.addEventListener('statechange',(e)=>{
console.log('statechange: '+JSON.stringify(e));
}, false);
},
// 开始推流
startPusher(){
this.beginlivepush()
},
beginlivepush() {
if(this.begin==false){//未开启推流
this.begin=true;//显示录制动画
// 设置推流服务器 ***此处需要通过ajax向后端获取
this.pusher.setOptions({
url:this.livepushurl //推流地址********************************* 此处设置推流地址
});
this.pusher.start();//推流开启
uni.showToast({
title: '开始录制',
icon:'none',
duration: 2000,
});
}else{
this.begin=false;//关闭录制动画
this.pusher.pause();;//暂停推流
uni.showToast({
title: '暂停录制',
icon:'none',
duration: 2000,
});
}
}
}
}
</script>
<style>
.content{
background: #000;
overflow: hidden;
}
.butlist{
height: 140upx;
position: absolute;
bottom: 0;
display: flex;
width: 100%;
justify-content: space-around;
padding-top: 20upx;
border-top: 1px solid #fff;
}
.buticon{
height: 120upx;
width: 120upx;
color: #fff;
position: relative;
text-align: center;
margin-bottom: 20upx;
}
.buticon image{
height: 64upx;
width: 64upx;
}
.buticon .mar10{
margin-top: -20upx;
}
.martp10{
margin-top: 10upx;
}
.give {
width: 90upx;
height: 90upx;
background: #F44336;
border-radius: 50%;
box-shadow: 0 0 22upx 0 rgb(252, 94, 20);
position: absolute;
left:15upx;
top:15upx;
font-size: 44upx;
line-height: 90upx;
}
.givebegin {
width: 60upx;
height: 60upx;
background: #F44336;
border-radius: 20%;
box-shadow: 0 0 22upx 0 rgb(252, 94, 20);
position: absolute;
left:30upx;
top:30upx;
}
.x_f{
/* border: 6upx solid #F44336; */
width: 120upx;
height: 120upx;
background: #fff;
border-radius: 50%;
position: absolute;
text-align: center;
top:0;
left: 0;
box-shadow: 0 0 28upx 0 rgb(251, 99, 24);
}
/* 产生动画(向外扩散变大)的圆圈 */
.pulse {
width: 160upx;
height: 160upx;
position: absolute;
border: 12upx solid #F44336;
border-radius: 100%;
z-index: 1;
opacity: 0;
-webkit-animation: warn 2s ease-out;
animation: warn 2s ease-out;
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
left: -28upx;
top: -28upx;
}
/**
* 动画
*/
@keyframes warn {
0% {
transform: scale(0);
opacity: 0.0;
}
25% {
transform: scale(0);
opacity: 0.1;
}
50% {
transform: scale(0.1);
opacity: 0.3;
}
75% {
transform: scale(0.5);
opacity: 0.5;
}
100% {
transform: scale(1);
opacity: 0.0;
}
}
</style>
web端播放(vue)
技术是 vue-video-player+videojs-contrib-hls 插件
<template>
<div class="container">
<video-player class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
>
</video-player>
</div>
</template>
<script>
import 'video.js/dist/video-js.css'
import { videoPlayer } from 'vue-video-player'
import'videojs-contrib-hls';
export default {
data(){
return {
playerOptions:{
autoplay: false,
muted: false,
loop: false,
preload: 'auto',
language: 'zh-CN',
aspectRatio: '16:9',
fluid: true,
sources: [{
type: "application/x-mpegURL",
src: "http://live.xxx.com/live/dfg.m3u8?txSecret=48ddddcddfs7adddd12c2c51ff978160e522&txTime=5E44DF78" //你的视频地址(必填)
}],
width: document.documentElement.clientWidth,
notSupportedMessage: '此视频暂无法播放,请稍后再试',
}
}
},
components: {
videoPlayer
},
methods: {
},
}
</script>
<style type="text/css" scoped>
.container {
background-color: #efefef;
height: 30%;
width:30%;
}
</style>