首先来看一下直播效果 推流工具有很多种(例如OBS、阿里云直播Demo推流、等等,我用的是芯象导播)阿里播放器地址
一、直播基础服务概述 官方文档说明
二、直播域名配置需要两个域名(推流域名、播流域名) 官方文档说明
四、开发流程
1、Java SDK安装 我用的是Apache Maven安装方式 官方安装说明文档
2、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";//推流域名配置的鉴权KeyString 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"; //播放鉴权KeyString 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、直播间互动功能(消息组)
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 {@ResourceLiveAppUtils liveAppUtils;@ResourceLiveRoomsMapper 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());}}
}
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、推流、断流回调
/*** 推流、断流回调地址*/@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;}
推流、断流日志
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}
直播记录