如果你也在面临做直播的 业务的时候迷茫的时候,来看看吧。
腾讯云–云直播
最近这几个项目也是在做直播相关的App开发,之前刚做的时候,自己也经历很多坑。
慢慢的爬过这些坑,做了几款直播软件以后,也算有了心得。更加熟练的调用。
分享自己的做直播的流程,以及配合腾讯云的直播官方文档的调用。
我做的这几个直播的项目是基于腾讯云的云直播服务进行开发的
- 腾讯云的账户分:个人账户和企业认证账户。
- 只有认证方式是不同的,其他的操作已经步骤以及思路都是一样的。
- 首先: 先去认证账户信息,进行实名认证的流程后,就可以使用腾讯云的服务了。
- 新的用户首次认证的时候,都可以免费体验腾讯云的云直播和即时通讯IM的服务(免费试用期一个月)
- 下面直接进入开发吧,不多啰嗦。
登录腾讯云的管理后台
- 点击云直播。(如果你是第一次使用腾讯云的云直播的话,你是需要去开通云直播服务后,才能可以进行使用)
-----现在看到的就是云直播的后台
1:首先,你要理解,直播是怎么回事?-----回答:直播利用的就是腾讯提供的云直播的服务,推流,拉流,客户端(主播)发起推流,创建直播房间成功后,创建IM群组成功后,客户端(用户)发起拉流。用户进入直播间可以看到主播的直播画面,这就是拉流成功。主播进行开播,开播后,主播可以看到自己的画面,能持续直播并且有画面,说明此时的推流地址是没问题的,可以进行直播。
2:其次就是怎么配置腾讯云的云直播后台的配置 ----回答:核心的配置,就是配置两条已经备案过的域名(一条域名用来做推流域名,另一条域名用来做拉流域名)
3:腾讯云的直播如何集成到自己的代码开发中去 —回答:集成自己的代码开发,这块我会在后面进行详细的讲解,以及代码的示范提供。
参考腾讯云的官方文档配置推拉流域名
新手指南
建议新手区参考新手指南,
先去看看,把直播的这两条推拉流域名配置好。
一条推流域名,一条拉流域名(都是需要去备案的)
其次就是要开通新手免费使用的流量包,没有流量包是不能进行直播的。
接下来需要了解,如何生成推流拉流地址。
先看腾讯云给出的生成规则。
只要你满足生成规则,你生成的流地址是正常的,就可以用来进行推拉流,进行正常的直播。
先看官网的地址生成器:
1:看看需要哪些参数
2:看看地址解析说明的配置
3:推拉流地址是要经过我们后台去代码生成的,然后返回给前端,安卓或者IOS,拿着地址去调用腾讯云的直播SDK,进行推流,拉流,完成APP端的主播,开播,用户进入直播间去观看,等功能。
具体看看,怎么生成推拉流的地址的
在这里,有官网提供的推拉流地址的生成代码
package com.test;import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;public class Test {public static void main(String[] args) {System.out.println(getSafeUrl("txrtmp", "11212122", 1469762325L));}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*/private 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();}private static String byteArrayToHexString(byte[] data) {char[] out = new char[data.length << 1];for (int i = 0, j = 0; i < data.length; i++) {out[j++] = DIGITS_LOWER[(0xF0 & data[i]) >>> 4];out[j++] = DIGITS_LOWER[0x0F & data[i]];}return new String(out);}
}
下面是我改进后的代码,大家可以作为参考:
package com.*.utils;import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;/*** 推拉流生成工具* @author * @date 2020-10-16 12:11:58*/
public class AutoAddressUtils {private static final char[] DIGITS_LOWER ={'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};private static final String pushKey = "f58*********d4f5d"; //域名管理中点击推流域名-->推流配置-->鉴权配置-->主KEYprivate static final String pushDomain = "*********"; //云直播控制台配置的推流域名private static final String pullDomain = "*********";//云直播控制台配置的拉流域名private static final String AppName = "live"; //直播SDK-->应用管理-->自己创建应用中的应用名称public static void main(String[] args) {long LiveRecordId = Long.parseLong(GenerateIdUtil.generateIdUtilId());System.out.println(LiveRecordId);LocalDateTime localDateTime = LocalDateTime.now();long nowTime = localDateTime.toEpochSecond(ZoneOffset.of("+8"));long endTime = nowTime + 60*60*12; // 默认12小时LiveAddress addressUrl = AutoAddressUtils.getAddressUrl(GenerateIdUtil.generateIdUtilId(), endTime);System.out.println(addressUrl.getPushRTMP());System.out.println(addressUrl.getPullUDP());System.out.println(addressUrl.getPullRTMP());System.out.println(addressUrl.getPullM3U8());System.out.println(addressUrl.getPullFLV());}public static LiveAddress getAddressUrl(String streamName, long txTime) {String safeUrl = getSafeUrl(pushKey, streamName, txTime);LiveAddress liveAddress = new LiveAddress();liveAddress.setPushRTMP("rtmp://"+pushDomain+"/"+AppName+"/"+streamName+"?"+safeUrl);liveAddress.setPullRTMP("rtmp://"+pullDomain+"/"+AppName+"/"+streamName+"?"+safeUrl);liveAddress.setPullFLV("http://"+pullDomain+"/"+AppName+"/"+streamName+".flv?"+safeUrl);liveAddress.setPullM3U8("http://"+pullDomain+"/"+AppName+"/"+streamName+".m3u8?"+safeUrl);liveAddress.setPullUDP("webrtc://"+pullDomain+"/"+AppName+"/"+streamName+"?"+safeUrl);return liveAddress;}/** KEY+ streamName + txTime*/private 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();}private static String byteArrayToHexString(byte[] data) {char[] out = new char[data.length << 1];for (int i = 0, j = 0; i < data.length; i++) {out[j++] = DIGITS_LOWER[(0xF0 & data[i]) >>> 4];out[j++] = DIGITS_LOWER[0x0F & data[i]];}return new String(out);}
}
1:生成推拉流的地址,前端调用云直播的SDK,拿到推拉流地址,就可以进行直播了。
2:因为我们是要集成代码去进行开发的,现在设计业务表。
3:创建一个直播场次表(live_record),既是每一次开播都是一个新的直播场次。
/*Navicat MySQL Data TransferSource Server : 192.168.1.105Source Server Type : MySQLSource Server Version : 50730Source Host : 111.6.79.10:3317Source Schema : liveTarget Server Type : MySQLTarget Server Version : 50730File Encoding : 65001Date: 16/10/2020 17:41:45
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for live_record
-- ----------------------------
DROP TABLE IF EXISTS `live_record`;
CREATE TABLE `live_record` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键的ID\r\n',`live_record_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '直播的场次ID',`anchor_code` bigint(20) NULL DEFAULT NULL COMMENT '主播code',`title` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '直播标题',`label` int(11) NULL DEFAULT NULL COMMENT '直播标签 分类',`live_cover` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '直播封面',`stream_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '流名称',`push_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '推流地址',`start_time` datetime(0) NULL DEFAULT NULL COMMENT '开播时间',`end_time` datetime(0) NULL DEFAULT NULL COMMENT '关播时间',`stream_end_time` datetime(0) NULL DEFAULT NULL COMMENT '流结束时间(流存活时间)',`showing` int(1) NULL DEFAULT 1 COMMENT '是否正在直播 1正在直播 0 已经直播',`recommend` int(1) NULL DEFAULT 1 COMMENT '是否推荐 0不推荐 1推荐',`watch_num` int(1) NULL DEFAULT 0 COMMENT '观看人数',`like_num` int(1) NULL DEFAULT 0 COMMENT '点赞人数',`location_longitude` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定位经度',`location_latitude` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定位纬度',`location_province` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定位省份',`location_city` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定位城市',`status` int(11) NULL DEFAULT 1 COMMENT '1正常 预留',`pull_rtmp_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '拉流地址',`pull_flv_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '拉流地址',`pull_m3u8_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '拉流地址',`pull_udp_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '拉流地址',`live_module` int(11) NULL DEFAULT NULL COMMENT '0城市1医疗2旅游',`room_number` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '直播的场次ID 和live_record_id值一样',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 105 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '直播场次表' ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;
1:设计完直播场次表(live_record)
2:开播接口---->开播的原理:APP端向java后台发送开播请求,主要是后台代码生成一条开播记录,以及对应的推流拉流地址。App拿到请求回来的(合法的推拉流)推拉流地址,然后调用腾讯云的SDK进行推流,如果调用SDK成功,且推流地址正确,则直播就能被创建成功。拉流也是一样的。调用腾讯云的SDK进行拉流,如果调用SDK成功,且拉流地址正确。直播就能被用户访问且观看。
3:关播接口 ---->关播的原理:APP端(即是安卓,IOS)调用腾讯云的关播断流的SDK,调用成功以后,且调用成功后,再回调java后台,修改后台自己设计的live_record 表的状态值(0:不显示 1:显示)。修改的是我们后台自己的表设计的状态值。
4:送佛送到西,写一套完整的直播的开播流程代码。
直播场次实体
/*** 直播场次表-->根据live_record表进行生成的* @author * @date 2020-10-16 12:11:58*/
@Data
@TableName("live_record")
public class LiveRecord implements Serializable {private static final long serialVersionUID = 1L;/*** 表的主键ID*/@TableId(type = IdType.AUTO)@NotNull(message = "表的主键ID"+"不能为空",groups = UpdateGroup.class)@ApiModelProperty(name = "id",value = "表的主键ID")private Long id;/*** 流名称*/@ApiModelProperty(name = "streamName",value = "流名称")private String streamName;/*** 直播的场次ID*/@ApiModelProperty(name = "liveRecordId",value = "直播的场次ID")private String liveRecordId;/*** 主播code*/@ApiModelProperty(name = "anchorCode",value = "主播code")private Long anchorCode;/*** 直播标题*/@ApiModelProperty(name = "title",value = "直播标题")private String title;/*** 直播标签 分类*/@ApiModelProperty(name = "label",value = "直播标签 分类")private Integer label;/*** 直播封面*/@ApiModelProperty(name = "liveCover",value = "直播封面")private String liveCover;/*** 推流地址*/@ApiModelProperty(name = "pushUrl",value = "推流地址")private String pushUrl;/*** 开播时间*/@ApiModelProperty(name = "startTime",value = "开播时间")private LocalDateTime startTime;/*** 关播时间*/@ApiModelProperty(name = "endTime",value = "关播时间")private LocalDateTime endTime;/*** 流结束时间*/@ApiModelProperty(name = "streamEndTime",value = "流结束时间")private LocalDateTime streamEndTime;/*** 是否正在直播 1正在直播 0 历史直播*/@ApiModelProperty(name = "showing",value = "是否正在直播 1正在直播 0 历史直播")private Integer showing;/*** 是否推荐 0不推荐 1推荐*/@ApiModelProperty(name = "isRecommend",value = "是否推荐 0不推荐 1推荐")private Integer recommend;/*** 观看人数*/@ApiModelProperty(name = "watchNum",value = "观看人数")private Integer watchNum;/*** 点赞人数*/@ApiModelProperty(name = "likeNum",value = "点赞人数")private Integer likeNum;/*** 定位经度*/@ApiModelProperty(name = "locationLongitude",value = "定位经度")private String locationLongitude;/*** 定位纬度*/@ApiModelProperty(name = "locationLatitude",value = "定位纬度")private String locationLatitude;/*** 定位省份*/@ApiModelProperty(name = "locationProvince",value = "定位省份")private String locationProvince;/*** 定位城市*/@ApiModelProperty(name = "locationCity",value = "定位城市")private String locationCity;/*** 1正常 预留*/@ApiModelProperty(name = "status",value = "1正常 预留")private Integer status;/*** 拉流地址*/@ApiModelProperty(name = "pullRtmpUrl",value = "拉流地址")private String pullRtmpUrl;/*** 拉流地址*/@ApiModelProperty(name = "pullFlvUrl",value = "拉流地址")private String pullFlvUrl;/*** 拉流地址*/@ApiModelProperty(name = "pullM3u8Url",value = "拉流地址")private String pullM3u8Url;/*** 拉流地址*/@ApiModelProperty(name = "pullUdpUrl",value = "拉流地址")private String pullUdpUrl;/*** 创建时间*/@ApiModelProperty(name = "createTime",value = "创建时间")private LocalDateTime createTime;/*** 更新时间*/@ApiModelProperty(name = "updateTime",value = "更新时间")private LocalDateTime updateTime;/*** 房间号*/@ApiModelProperty(name = "roomNumber",value = "房间号")private String roomNumber;
}
封装一个推拉流地址实体
package com.*.utils;/*** 推拉流地址*/
public class LiveAddress {private String pushRTMP;private String pullRTMP;private String pullFLV;private String pullM3U8;private String pullUDP;public String getPushRTMP() {return pushRTMP;}public void setPushRTMP(String pushRTMP) {this.pushRTMP = pushRTMP;}public String getPullRTMP() {return pullRTMP;}public void setPullRTMP(String pullRTMP) {this.pullRTMP = pullRTMP;}public String getPullFLV() {return pullFLV;}public void setPullFLV(String pullFLV) {this.pullFLV = pullFLV;}public String getPullM3U8() {return pullM3U8;}public void setPullM3U8(String pullM3U8) {this.pullM3U8 = pullM3U8;}public String getPullUDP() {return pullUDP;}public void setPullUDP(String pullUDP) {this.pullUDP = pullUDP;}}
直播场次生成工具
package com.*.utils;import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;public class GenerateIdUtil {// 使用单例模式,不允许直接创建实例private GenerateIdUtil() {}// 创建一个空实例对象,类需要用的时候才赋值private static GenerateIdUtil instance = null;// 单例模式--懒汉模式public static synchronized GenerateIdUtil getInstance() {if (instance == null) {instance = new GenerateIdUtil();}return instance;}// 全局自增数private static int count = 1;// 格式化的时间字符串private static final SimpleDateFormat sdf = new SimpleDateFormat("ddHHmmss");// 获取当前时间年月日时分秒毫秒字符串private static String getNowDateStr() {return sdf.format(new Date());}// 记录上一次的时间,用来判断是否需要递增全局数private static String now = null;//定义锁对象private final static ReentrantLock lock=new ReentrantLock();/*** 随机生成11位数字的随机数* @return*/public static String generateIdUtilId(){String Newnumber=null;String dateStr=getNowDateStr();lock.lock();//加锁//判断是时间是否相同if (dateStr.equals(now)) {try {if (count >= 10000){count = 1;}if (count<10) {Newnumber = getNowDateStr()+"00"+count;}else if (count<100) {Newnumber = getNowDateStr()+"0"+count;} else {Newnumber = getNowDateStr()+count;}count++;} catch (Exception e) {}finally{lock.unlock();}}else{count=1;now =getNowDateStr();try {if (count >= 10000){count = 1;}if (count<10) {Newnumber = getNowDateStr()+"00"+count;}else if (count<100) {Newnumber = getNowDateStr()+"0"+count;}else {Newnumber = getNowDateStr()+count;}count++;} catch (Exception e) {}finally{lock.unlock();}}return Newnumber;//返回的值}/*** 生成32位的随机的数字* @return*/public static String generateRandomTO32(int num) {String chars = "0123456789";char[] rands = new char[num];for (int i = 0; i < num; i++) {int rand = (int) (Math.random() * 10);rands[i] = chars.charAt(rand);}return String.valueOf(rands);}
}
直播的开播与关播
Controller层
package com.*.controller;import com.*.entity.LiveRecord;
import com.*.result.ResultObject;
import com.*.service.LiveBroadcastService;
import com.*.vo.LiveBroadCastVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.*;/*** 开播的模块* @author * @date 2020-10-16 12:11:58*/
@RequestMapping("/liveBroadCast")
public class LiveBroadCastController {@Autowiredprivate LiveBroadcastService liveBroadcastService;@ApiOperation("开播")@PostMapping("/open")public ResultObject open(@RequestBody LiveRecord liveRecord) {ResultObject resultObject = liveBroadcastService.open(liveRecord);return resultObject;}@ApiOperation("关播")@PostMapping("/close")public ResultObject close(@RequestBody LiveBroadCastVO liveBroadCastVO) {ResultObject resultObject = liveBroadcastService.close(liveBroadCastVO);return resultObject;}
}
service层
package com.*.service;
import com.*.entity.LiveRecord;
import com.*.result.ResultObject;
import com.*.vo.LiveBroadCastVO;/*** APP端开播模块* @author * @date 2020-10-16 12:11:58*/
public interface LiveBroadcastService {/*** 开播* @param liveRecord* @return*/ResultObject open(LiveRecord liveRecord);/*** 关播* @param liveBroadCastVO* @return*/ResultObject close(LiveBroadCastVO liveBroadCastVO);}
impl
/*** APP端开播模块* @author * @date 2020-10-16 12:11:58*/
@Service("LiveBroadcastService")
public class LiveBroadcastServiceImpl implements LiveBroadcastService {@Resourceprivate LiveRecordMapper liveRecordMapper; //这个接口自己去实现@Transactional@Overridepublic ResultObject open(LiveRecord liveRecord) {if (liveRecord.getTitle() == null) {return ResultObject.error("直播标题不能为空");}if (liveRecord.getLocationLatitude() == null || liveRecord.getLocationLongitude() == null) {return ResultObject.error("直播定位没有开启");}if (liveRecord.getLiveCover() == null) {return ResultObject.error("直播的封面未上传");}//开播LocalDateTime localDateTime = LocalDateTime.now();long nowTime = localDateTime.toEpochSecond(ZoneOffset.of("+8"));
// long endTime = nowTime + 60*60*2;long endTime = nowTime + 60*3;LiveRecord liveRecord1 = new LiveRecord();String liveRecordId = GenerateIdUtil.generateIdUtilId();liveRecord1.setLiveRecordId(liveRecordId);liveRecord1.setTitle(liveRecord.getTitle());liveRecord1.setLiveCover(liveRecord.getLiveCover());liveRecord1.setAnchorCode(liveRecord.getAnchorCode());
/* String toString = liveRecord.getAnchorCode().toString();String substring = toString.substring(toString.length()-6, toString.length());*/
// liveRecord1.setRoomNumber(Integer.parseInt(substring));liveRecord1.setRoomNumber(liveRecordId);liveRecord1.setLocationProvince(liveRecord.getLocationProvince());liveRecord1.setLocationCity(liveRecord.getLocationCity());liveRecord1.setLabel(liveRecord.getLabel());liveRecord1.setLiveModule(liveRecord.getLiveModule());liveRecord1.setStartTime(localDateTime);liveRecord1.setCreateTime(localDateTime);liveRecord1.setShowing(0); // 默认生成的直播不显示liveRecord1.setStreamEndTime(Instant.ofEpochSecond(endTime).atOffset(ZoneOffset.of("+08:00")).toLocalDateTime());liveRecord1.setLocationLatitude(liveRecord.getLocationLatitude());liveRecord1.setLocationLongitude(liveRecord.getLocationLongitude());LiveAddress addressUrl =AutoAddressUtils.getAddressUrl(liveRecordId, endTime);liveRecord1.setStreamName(liveRecordId);liveRecord1.setPushUrl(addressUrl.getPushRTMP());liveRecord1.setPullFlvUrl(addressUrl.getPullFLV());liveRecord1.setPullM3u8Url(addressUrl.getPullM3U8());liveRecord1.setPullRtmpUrl(addressUrl.getPullRTMP());liveRecord1.setPullUdpUrl(addressUrl.getPullUDP());try {liveRecordMapper.insert(liveRecord1); //自己写一个直播场次数据插入的接口} catch (Exception e) {e.printStackTrace();}return ResultObject.ok(liveRecord1);}@Transactional@Overridepublic ResultObject close(LiveBroadCastVO liveBroadCastVO) {// 修改直播场次的状态if (liveBroadCastVO.getLiveRecordId() == null) {return ResultObject.error("参数异常");}//修改状态值为0liveRecordMapper.updateShowing(liveBroadCastVO); //自己写一个修改直播的状态值的接口return ResultObject.ok("关播成功", null);}
}
如果不考虑直播的其他的一些异常的情况的话,简单的额直播的开播,关播,已经做好了。
把写好的开播,关播接口,提供给APP就行。
如果需要额外帮助:WX:380666989