流程分析
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>