首先来看一下直播效果 推流工具有很多种(例如OBS、阿里云直播Demo推流、等等,我用的是芯象导播)阿里播放器地址
一、直播基础服务概述 官方文档说明
二、直播域名配置需要两个域名(推流域名、播流域名) 官方文档说明
四、开发流程
1、Java SDK安装 我用的是Apache Maven安装方式 官方安装说明文档
2、Java代码生成推流地址和播放地址 官方案例
java
package com.zaiyun.zhibo.utils;
import com.zaiyun.common.core.domain.model.User;
import com.zaiyun.common.utils.DateUtils;
import com.zaiyun.common.utils.SecurityUtils;
import com.zaiyun.zhibo.domain.LiveRooms;
import org.springframework.stereotype.Component;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
/**
* 阿里视频直播工具类
*/
@Component
public class LiveUtils {
/**
* 生成推流地址
*
* @param appName 推流AppName
* @param streamName 推流StreamName
*/
public static String buildPushUrl(String appName, String streamName) {
String pushDomain = "zyt.*****.com"; //推流域名
String pushKey = "2T39rSP2dVro";//推流域名配置的鉴权Key
String pushUrl = "";
//推流域名未开启鉴权功能的情况下
if (pushKey == "") {
pushUrl = "rtmp://" + pushDomain + "/" + appName + "/" + streamName;
} else {
Long timeStamp = System.currentTimeMillis() / 1000L + 3601L;
String stringToMd5 = "/" + appName + "/" + streamName + "-" + Long.toString(timeStamp) + "-0-0-" + pushKey;
String authKey = md5(stringToMd5);
pushUrl = "rtmp://" + pushDomain + "/" + appName + "/" + streamName + "?auth_key=" + Long.toString(timeStamp) + "-0-0-" + authKey;
}
return pushUrl;
}
/**
* 生成播放地址
*
* @param appName 播放appName(同推流appName)
* @param streamName 播放streamName,播放源流时,streamName 同推流streamName;播放转码流时,streamName 为推流streamName_{转码模板ID}
*/
public static HashMap<String, String> buildPullUrl(String appName, String streamName) {
String pullDomain = "zyb.*****.com"; //播放域名
String pullKey = "92KiuYjNYr5H"; //播放鉴权Key
String rtmpUrl = ""; //rtmp的拉流地址
String hlsUrl = ""; //m3u8的拉流地址
String flvUrl = ""; //flv的拉流地址
String rtsUrl = ""; //rts的拉流地址
//播放域名未配置鉴权Key的情况下
if (pullKey == "") {
rtmpUrl = "rtmp://" + pullDomain + "/" + appName + "/" + streamName;
rtsUrl = "artc://" + pullDomain + "/" + appName + "/" + streamName;
hlsUrl = "http://" + pullDomain + "/" + appName + "/" + streamName + ".m3u8";
flvUrl = "http://" + pullDomain + "/" + appName + "/" + streamName + ".flv";
} else {
Long timeStamp = System.currentTimeMillis() / 1000L + 3600L;
String rtmpToMd5 = "/" + appName + "/" + streamName + "-" + Long.toString(timeStamp) + "-0-0-" + pullKey;
String rtmpAuthKey = md5(rtmpToMd5);
rtmpUrl = "rtmp://" + pullDomain + "/" + appName + "/" + streamName + "?auth_key=" + Long.toString(timeStamp) + "-0-0-" + rtmpAuthKey;
String hlsToMd5 = "/" + appName + "/" + streamName + ".m3u8-" + Long.toString(timeStamp) + "-0-0-" + pullKey;
String hlsAuthKey = md5(hlsToMd5);
hlsUrl = "http://" + pullDomain + "/" + appName + "/" + streamName + ".m3u8" + "?auth_key=" + Long.toString(timeStamp) + "-0-0-" + hlsAuthKey;
String flvToMd5 = "/" + appName + "/" + streamName + ".flv-" + Long.toString(timeStamp) + "-0-0-" + pullKey;
String flvAuthKey = md5(flvToMd5);
flvUrl = "http://" + pullDomain + "/" + appName + "/" + streamName + ".flv" + "?auth_key=" + Long.toString(timeStamp) + "-0-0-" + flvAuthKey;
String rtsToMd5 = "/" + appName + "/" + streamName + "-" + Long.toString(timeStamp) + "-0-0-" + pullKey;
String rtsAuthKey = md5(rtsToMd5);
rtsUrl = "artc://" + pullDomain + "/" + appName + "/" + streamName + "?auth_key=" + Long.toString(timeStamp) + "-0-0-" + rtsAuthKey;
}
HashMap<String, String> url = new HashMap<>();
url.put("rtmpUrl", rtmpUrl);
url.put("hlsUrl", hlsUrl);
url.put("flvUrl", flvUrl);
url.put("rtsUrl", rtsUrl);
return url;
}
/**
* 计算md5
*/
public static String md5(String param) {
if (param == null || param.length() == 0) {
return null;
}
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(param.getBytes());
byte[] byteArray = md5.digest();
BigInteger bigInt = new BigInteger(1, byteArray);
// 参数16表示16进制
String result = bigInt.toString(16);
// 不足32位高位补零
while (result.length() < 32) {
result = "0" + result;
}
return result;
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
/**
* 登录需的要鉴权信息
*/
public static HashMap<String, Object> appAuth(LiveRooms room) {
String role = "";
User user = SecurityUtils.getLoginUser().getUser();
String userId = user.getUserId().toString();
String appId = room.getAppId();
String appKey = room.getAppKey();
String appSign = room.getAppSign();
String nonce = java.util.UUID.randomUUID().toString();
Long timestamp = DateUtils.addHours(new Date(), 1).getTime() / 1000;
String signContent = String.format("%s%s%s%s%s%s", appId, appKey, userId, nonce, timestamp, role);
String appToken = org.apache.commons.codec.digest.DigestUtils.sha256Hex(signContent);
HashMap<String, Object> result = new HashMap<>();
result.put("appId", appId);
result.put("appSign", appSign);
result.put("appToken", appToken);
result.put("role", role);
result.put("nonce", nonce);
result.put("userId", userId);
result.put("timestamp", timestamp);
result.put("userName", user.getUserName());
return result;
}
}
3、直播间互动功能(消息组)
java
package com.zaiyun.zhibo.controller;
import com.zaiyun.common.annotation.Anonymous;
import com.zaiyun.common.core.controller.BaseController;
import com.zaiyun.common.core.domain.AjaxResult;
import com.zaiyun.zhibo.domain.LiveRooms;
import com.zaiyun.zhibo.mapper.LiveRoomsMapper;
import com.zaiyun.zhibo.utils.LiveAppUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
/**
* 最新版本的消息组
*/
@Anonymous
@RestController
@RequestMapping("/live/app")
public class LiveAppController extends BaseController {
@Resource
LiveAppUtils liveAppUtils;
@Resource
LiveRoomsMapper liveRoomsMapper;
/**
* 创建互动消息应用
*/
@PostMapping("/create")
public AjaxResult createMessageApp() {
try {
String appName = "myApp";
return success(liveAppUtils.createMessageApp(appName));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 获取互动消息应用
*/
@PostMapping("/list")
public AjaxResult getMessageAppList() {
try {
return success(liveAppUtils.getMessageAppList());
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 获取互动消息应用详情
*/
@PostMapping("/info")
public AjaxResult getMessageAppInfo() {
try {
String appId = "abc13a82b773";
return success(liveAppUtils.getMessageAppInfo(appId));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 创建消息组
*/
@PostMapping("/group/create")
public AjaxResult createMessageGroup() {
try {
String appId = "abc13a82b773";
return success(liveAppUtils.createMessageGroup(appId));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 查询指定用户下消息组列表
*/
@PostMapping("/group/list")
public AjaxResult listMessageGroup() {
try {
String appId = "abc13a82b773";
return success(liveAppUtils.listMessageGroup(appId));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 消息组详情
*/
@PostMapping("/group/describe")
public AjaxResult groupDescribe() {
try {
HashMap<String, String> parameter = new HashMap<>();
parameter.put("appId", "abc13a82b773");
parameter.put("groupId", "211a39c8-2ebd-4d4b-978e-dfa31836220f");
return success(liveAppUtils.groupDescribe(parameter));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 发送消息到群组
*/
@PostMapping("/send/group")
public AjaxResult sendMessageGroup(@RequestBody HashMap<String, String> data) {
try {
String roomId = data.get("roomId");
String msgType = data.get("msgType");
String body = data.get("body");
if (roomId == null || msgType == null || body == null) {
return AjaxResult.error(201, "参数错误 roomId 或 msgType 或 body");
}
LiveRooms liveRooms = new LiveRooms();
liveRooms.setId(Integer.parseInt(roomId));
LiveRooms room = liveRoomsMapper.findRoomByWhere(liveRooms);
HashMap<String, String> parameter = new HashMap<>();
parameter.put("appId", room.getAppId());
parameter.put("groupId", room.getGroupId());
parameter.put("senderId", "admin" + getUserId());//发送者
parameter.put("msgType", msgType);//消息类型
parameter.put("body", body);//消息内容
return success(liveAppUtils.sendMessageGroup(parameter));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 发送消息到用户
*/
@PostMapping("/send/user")
public AjaxResult sendMessageUser() {
try {
HashMap<String, String> parameter = new HashMap<>();
parameter.put("appId", "abc13a82b773");
parameter.put("senderId", "uid10");//发送者
parameter.put("receiverId", "100");//接收者
parameter.put("body", "{\"content\":\"这里是发送的消息内容!\"}");//消息内容
return success(liveAppUtils.sendMessageUser(parameter));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 查询群组消息列表
*/
@PostMapping("/group/messages")
public AjaxResult getGroupMessages() {
try {
HashMap<String, String> parameter = new HashMap<>();
parameter.put("appId", "abc13a82b773");
parameter.put("groupId", "211a39c8-2ebd-4d4b-978e-dfa31836220f");
return success(liveAppUtils.getGroupMessages(parameter));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
/**
* 查询群组用户列表
*/
@PostMapping("/group/users")
public AjaxResult getGroupUsers() {
try {
HashMap<String, String> parameter = new HashMap<>();
parameter.put("appId", "abc13a82b773");
parameter.put("groupId", "211a39c8-2ebd-4d4b-978e-dfa31836220f");
return success(liveAppUtils.getGroupUsers(parameter));
} catch (Exception e) {
return AjaxResult.error(201, e.getMessage());
}
}
}
java
package com.zaiyun.zhibo.utils;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.live.model.v20161101.*;
import com.aliyuncs.profile.DefaultProfile;
import com.zaiyun.common.config.AlibabaConfig;
import com.zaiyun.common.utils.uuid.UUID;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
/**
* 阿里视频直播工具类
*/
@Component
public class LiveAppUtils {
/**
* 初始化配置
*/
public static IAcsClient createClient() throws Exception {
DefaultProfile profile = DefaultProfile.getProfile("cn-shanghai", AlibabaConfig.getAccessKeyId(), AlibabaConfig.getAccessKeySecret());
return new DefaultAcsClient(profile);
}
/**
* 创建互动消息应用
*
* @param appName 互动消息应用名称,长度 2~16 个字符
*/
public static CreateLiveMessageAppResponse createMessageApp(String appName) throws Exception {
IAcsClient client = createClient();
CreateLiveMessageAppRequest request = new CreateLiveMessageAppRequest();
request.setAppName(appName);
request.setAuditType(1);//内置安全审核
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("创建互动消息应用异常!" + error);
}
}
/**
* 查询互动消息应用列表
*/
public static Object getMessageAppList() throws Exception {
IAcsClient client = createClient();
ListLiveMessageAppsRequest request = new ListLiveMessageAppsRequest();
request.setSortType(1);
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("查询互动消息应用列表异常!" + error);
}
}
/**
* 查询互动消息应用详情
*/
public static Object getMessageAppInfo(String appId) throws Exception {
IAcsClient client = createClient();
DescribeLiveMessageAppRequest request = new DescribeLiveMessageAppRequest();
request.setAppId(appId);
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("查询互动消息应用详情异常!" + error);
}
}
/**
* 创建消息组
*
* @param appId 互动消息应用ID
*/
public static String createMessageGroup(String appId) throws Exception {
IAcsClient client = createClient();
CreateLiveMessageGroupRequest request = new CreateLiveMessageGroupRequest();
request.setAppId(appId);
request.setCreatorId("admin1");//群组创建者ID。
request.setDataCenter("cn-shanghai");//数据中心
request.setGroupInfo("testgroupinfo");//群组扩展信息,最大512字符。
request.setGroupId(UUID.fastUUID().toString());//要创建的群组ID,由大小写字母、数字组成,最大64字符。
request.setGroupName(RandomStringUtils.randomAlphanumeric(10));//群组名,最大64字符。
ArrayList<String> administrators = new ArrayList<>();
administrators.add("aaaaa");
administrators.add("bbbbb");
administrators.add("ccccc");
request.setAdministrators(administrators);//管理员用户ID数组
try {
CreateLiveMessageGroupResponse response = client.getAcsResponse(request);
return response.getGroupId();
} catch (ClientException error) {
throw new RuntimeException("创建消息组异常!" + error);
}
}
/**
* 查询指定用户下消息组列表
*
* @param appId 互动消息应用ID
*/
public static Object listMessageGroup(String appId) throws Exception {
IAcsClient client = createClient();
ListLiveMessageGroupsRequest request = new ListLiveMessageGroupsRequest();
request.setAppId(appId);
request.setSortType(2);
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("查询指定用户下消息组列表异常!" + error);
}
}
/**
* 消息组详情
*
* @param parameter 请求参数
*/
public static Object groupDescribe(HashMap<String, String> parameter) throws Exception {
IAcsClient client = createClient();
DescribeLiveMessageGroupRequest request = new DescribeLiveMessageGroupRequest();
request.setAppId(parameter.get("appId"));
request.setGroupId(parameter.get("groupId"));
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("查询指定用户下消息组列表异常!" + error);
}
}
/**
* 发送消息到群组
*
* @param parameter 请求参数
*/
public static Object sendMessageGroup(HashMap<String, String> parameter) throws Exception {
IAcsClient client = createClient();
SendLiveMessageGroupRequest request = new SendLiveMessageGroupRequest();
request.setAppId(parameter.get("appId"));
request.setGroupId(parameter.get("groupId"));
request.setSenderId(parameter.get("senderId"));//发送者
request.setBody(parameter.get("body"));//消息内容
request.setMsgType(Long.parseLong(parameter.get("msgType")));//消息类型
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("发送消息到群组异常!" + error);
}
}
/**
* 发送消息到用户
*
* @param parameter 请求参数
*/
public static Object sendMessageUser(HashMap<String, String> parameter) throws Exception {
IAcsClient client = createClient();
SendLiveMessageUserRequest request = new SendLiveMessageUserRequest();
request.setAppId(parameter.get("appId"));
request.setSenderId(parameter.get("senderId"));//发送者
request.setReceiverId(parameter.get("receiverId"));//接收者
request.setBody(parameter.get("body"));//消息内容
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("发送消息到用户异常!" + error);
}
}
/**
* 查询群组消息列表
*
* @param parameter 请求参数
*/
public static Object getGroupMessages(HashMap<String, String> parameter) throws Exception {
IAcsClient client = createClient();
ListLiveMessageGroupMessagesRequest request = new ListLiveMessageGroupMessagesRequest();
request.setAppId(parameter.get("appId"));
request.setGroupId(parameter.get("groupId"));
request.setSortType(2);
request.setPageSize(10);
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("查询群组消息列表异常!" + error);
}
}
/**
* 查询群组用户列表
*
* @param parameter 必须参数
*/
public static Object getGroupUsers(HashMap<String, String> parameter) throws Exception {
IAcsClient client = createClient();
ListLiveMessageGroupUsersRequest request = new ListLiveMessageGroupUsersRequest();
request.setAppId(parameter.get("appId"));
request.setGroupId(parameter.get("groupId"));
request.setSortType(2);
request.setPageSize(30);
try {
return client.getAcsResponse(request);
} catch (ClientException error) {
throw new RuntimeException("查询群组用户列表异常!" + error);
}
}
}
4、推流、断流回调
java
/**
* 推流、断流回调地址
*/
@GetMapping("/callback")
public void callback(HttpServletRequest request) {
try {
//处理回调请求参数
HashMap<String, Object> paramsMap = ConvertUtils.getRequestData(request);
//开始推流
if (paramsMap.get("action").equals("publish")) {
tiktokLogger.info("开始推流:" + paramsMap);
liveRoomsLogService.addRoomLog(paramsMap);
}
//推流中断
if (paramsMap.get("action").equals("publish_done")) {
tiktokLogger.info("推流中断:" + paramsMap);
liveRoomsLogService.upRoomLog(paramsMap);
}
} catch (Exception e) {
tiktokLogger.info("回调异常:" + e.getMessage());
}
}
/**
* 处理请求参数
*
* @param request 请求
* @return 结果
*/
public static HashMap<String, Object> getRequestData(HttpServletRequest request) {
HashMap<String, Object> paramsMap = new HashMap<>(); //重新定义请求的参数
Map<String, String[]> map = request.getParameterMap(); //请求中的map数组
for (String key : map.keySet()) { //遍历数组
String[] value = map.get(key);
if (value.length == 1) {
paramsMap.put(key, map.get(key)[0]);
} else {
paramsMap.put(key, value);
}
}
return paramsMap;
}
推流、断流日志
java
09:44:32.770 [http-nio-8082-exec-13] INFO extend-tiktok - [callback,44] - 开始推流:{app=zyt.***.com, node=117.49.93.167, appname=TtSCpj, width=1080, action=publish, id=D6v3Su5mMQ, time=1723599872, usrargs=vhost=zyb.***.com&auth_key=1723603295-0-0-487e7adb13e00bdda7e7c6cbd97f88f8&ali_publisher&ali_edge_node_ip=117.49.93.167&ali_node_via=live13.cn4435%2clive15.l2et135-3&ali_node_ip=10.120.24.162#26%2c118.178.204.227#6&alilive_streamidv2=live13.cn4435_2955_3699959155_1723599872186&alilive_clienthost=live15.l2et135-3&orig_tc_url=rtmp://zyt.***.com/TtSCpj, height=1920}
09:46:06.204 [http-nio-8082-exec-15] INFO extend-tiktok - [callback,50] - 推流中断:{app=zyt.***.com, node=117.49.93.167, appname=TtSCpj, width=1080, action=publish_done, id=D6v3Su5mMQ, time=1723599966, usrargs=vhost=zyb.***.com&auth_key=1723603295-0-0-487e7adb13e00bdda7e7c6cbd97f88f8&ali_publisher_ip=&ali_edge_node_ip=117.49.93.167&ali_node_via=live13.cn4435%2clive15.l2et135-3&ali_node_ip=10.120.24.162#26%2c118.178.204.227#6&alilive_streamidv2=live13.cn4435_2955_3699959155_1723599872186&alilive_clienthost=live15.l2et135-3&orig_tc_url=rtmp://zyt.***.com/TtSCpj, height=1920}
09:46:25.407 [http-nio-8082-exec-64] INFO extend-tiktok - [callback,44] - 开始推流:{app=zyt.***.com, node=117.49.93.167, appname=Srvv66, ip=, width=1080, action=publish, id=b974e1hqmy, time=1723599985, usrargs=vhost=zyb.***.com&auth_key=1723603558-0-0-c54c7bad6bde755300bac0efb5ae44b7&ali_publisher_ip=&ali_edge_node_ip=117.49.93.167&ali_node_via=live6.cn4435%2clive3.l2et135-3&ali_node_ip=10.120.24.217#26%2c118.178.204.215#6&alilive_streamidv2=live6.cn4435_3485_3698551537_1723599984805&alilive_clienthost=live3.l2et135-3&orig_tc_url=rtmp://zyt.***.com/Srvv66, height=1920}
09:53:58.001 [http-nio-8082-exec-53] INFO extend-tiktok - [callback,50] - 推流中断:{app=zyt.***.com, node=117.49.93.167, appname=Srvv66, ip=, width=1080, action=publish_done, id=b974e1hqmy, time=1723600437, usrargs=vhost=zyb.***.com&auth_key=1723603558-0-0-c54c7bad6bde755300bac0efb5ae44b7&ali_publisher_ip=&ali_edge_node_ip=117.49.93.167&ali_node_via=live6.cn4435%2clive3.l2et135-3&ali_node_ip=10.120.24.217#26%2c118.178.204.215#6&alilive_streamidv2=live6.cn4435_3485_3698551537_1723599984805&alilive_clienthost=live3.l2et135-3&orig_tc_url=rtmp://zyt.***.com/Srvv66, height=1920}
11:01:52.914 [http-nio-8082-exec-18] INFO extend-tiktok - [callback,44] - 开始推流:{app=zyt.***.com, node=58.222.29.233, appname=Srvv66, ip=, width=1080, action=publish, id=b974e1hqmy, time=1723604512, usrargs=vhost=zyb.***.com&auth_key=1723603558-0-0-c54c7bad6bde755300bac0efb5ae44b7&ali_publisher_ip=&ali_edge_node_ip=58.222.29.233&ali_node_via=live1.cn3421%2clive15.l2et135-3&ali_node_ip=10.120.25.6#26%2c118.178.204.227#6&alilive_streamidv2=live1.cn3421_76677_2481419939_1723604511502&alilive_clienthost=live15.l2et135-3&orig_tc_url=rtmp://zyt.***.com/Srvv66, height=1920}
11:14:48.320 [http-nio-8082-exec-65] INFO extend-tiktok - [callback,50] - 推流中断:{app=zyt.***.com, node=58.222.29.233, appname=Srvv66, ip=, width=1080, action=publish_done, id=b974e1hqmy, time=1723605288, usrargs=vhost=zyb.***.com&auth_key=1723603558-0-0-c54c7bad6bde755300bac0efb5ae44b7&ali_publisher_ip=&ali_edge_node_ip=58.222.29.233&ali_node_via=live1.cn3421%2clive15.l2et135-3&ali_node_ip=10.120.25.6#26%2c118.178.204.227#6&alilive_streamidv2=live1.cn3421_76677_2481419939_1723604511502&alilive_clienthost=live15.l2et135-3&orig_tc_url=rtmp://zyt.***.com/Srvv66, height=1920}
直播记录