视频直播服务(ApsaraVideo Live)是基于领先的内容接入、分发网络和大规模分布式实时转码技术打造的音视频直播平台,提供便捷接入、高清流畅、低延迟、高并发的音视频直播服务。本教程指引您如何使用视频直播服务搭建视频直播,并将直播录制视频保存到阿里云OSS上。

前提条件

在使用本教程之前,请您务必完成以下操作:
  1. 确保您已开通了视频直播服务。请参见开通视频直播服务
  2. 使用Alibaba Cloud SDK for Java,您需要一个阿里云账号和访问密钥(AccessKey)。 请在阿里云控制台中的AccessKey管理页面上创建和查看您的AccessKey。

注意事项

本文有以下两点需要您注意:

  1. 在本文示例代码参数中推流域名拉流(播流)域名AppName请保持全局唯一。
  2. 本文基于Alibaba Cloud SDK for java完成,代码示例中所涉及的maven依赖有Aliyun-Java-SDK-CoreAliyun-Java-SDK-Live
    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-core -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.4.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-live -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-live</artifactId>
            <version>3.8.0</version>
        </dependency>
    </dependencies>

步骤一 添加视频直播域名

添加视频直播域名,您需要调用AddLiveDomain接口添加一个推流域名(业务类型为liveEdge)和一个播放域名(业务类型为liveVideo),然后再调用AddLiveDomainMapping接口将已创建的推流域名和播放域名关联。

完整代码示例如下:

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.live.model.v20161101.*;
import com.aliyuncs.profile.DefaultProfile;
import com.google.gson.Gson;

public class TestStartLive {

    // 推流域名
    private static String liveEdge = "www.yourLiveEdge.club";
    // 播流域名
    private static String liveVideo = "www.yourLiveVideo.club";

    /**
     * Initialization  初始化公共请求参数
     */
    public IAcsClient initialization() {
        // 初始化请求参数
        DefaultProfile profile = DefaultProfile.getProfile(
                "<your-region-id>", // 您的可用区ID
                "<your-access-key-id>", // 您的AccessKey ID
                "<your-access-key-secret>"); // 您的AccessKey Secret
        return new DefaultAcsClient(profile);
    }

    /**
     * 添加直播域名 AddLiveDomain
     *
     * @param client         公共请求参数
     * @param domainName     需要接入直播的域名。支持泛域名,以符号“.”开头,如:.a.com。
     * @param liveDomainType 域名业务类型。取值:liveVideo:播流域名  liveEdge:边缘推流域名
     * @param scope          加速区域。国际用户、国内L3及以上用户设置有效。
     */
    public AddLiveDomainResponse addLiveDomain(IAcsClient client, String domainName, String liveDomainType, String scope, String region) throws ClientException {
        AddLiveDomainRequest request = new AddLiveDomainRequest();
        System.out.println("--------------------addLiveDomain--------------------");
        request.setDomainName(domainName);
        request.setRegion(region);
        request.setLiveDomainType(liveDomainType);
        request.setScope(scope);
        return client.getAcsResponse(request);
    }

    /**
     * AddLiveDomainMapping  配置推流和拉流的映射关系
     *
     * @param client     公共请求参数
     * @param pullDomain 播流域名,域名类型为liveVideo。
     * @param pushDomain 推流域名,域名类型为liveEdge。
     */
    public AddLiveDomainMappingResponse addLiveDomainMapping(IAcsClient client, String pullDomain, String pushDomain) throws ClientException {
        AddLiveDomainMappingRequest request = new AddLiveDomainMappingRequest();
        System.out.println("--------------------addLiveDomainMapping--------------------");
        request.setPullDomain(pullDomain);
        request.setPushDomain(pushDomain);
        return client.getAcsResponse(request);
    }

    /**
     * DescribeLiveUserDomains 查询用户名下所有的直播域名
     *
     * @param client 公共请求参数
     */
    public DescribeLiveUserDomainsResponse describeLiveUserDomains(IAcsClient client) throws ClientException {
        DescribeLiveUserDomainsRequest request = new DescribeLiveUserDomainsRequest();
        System.out.println("--------------------describeLiveUserDomains--------------------");
        // request.setDomainName(domainName);
        return client.getAcsResponse(request);
    }

    public static void main(String[] args) {
        TestStartLive live = new TestStartLive();
        Gson gson = new Gson();
        IAcsClient client = live.initialization();
        try {
            // 添加推流域名。
            AddLiveDomainResponse liveEdgeResponse = live.addLiveDomain(client, liveEdge, "liveEdge", "domestic","cn-shanghai");
            System.out.println(gson.toJson(liveEdgeResponse));
            // 添加播流域名。
            AddLiveDomainResponse liveVideoResponse = live.addLiveDomain(client, liveVideo, "liveVideo", "domestic","cn-shanghai");
            System.out.println(gson.toJson(liveVideoResponse));
            // 查询直播域名列表。
            // 判断确认域名是否添加成功,且状态为online。
            while (true) {
                DescribeLiveUserDomainsResponse userDomains = live.describeLiveUserDomains(client);
                String liveEdgeStatus = null;
                String liveVideoStatus = null;
                for (DescribeLiveUserDomainsResponse.PageData pageData : userDomains.getDomains()) {
                    String domainName = pageData.getDomainName();
                    // 提取推流域名状态
                    if (domainName.equals(liveEdge)) {
                        liveEdgeStatus = pageData.getLiveDomainStatus();
                    }
                    // 提取拉流域名状态
                    if (domainName.equals(liveVideo)) {
                        liveVideoStatus = pageData.getLiveDomainStatus();
                    }
                }
                // 判断确认域名是否添加成功,且状态为online。
                if (liveEdgeStatus.equals("online") && liveVideoStatus.equals("online")) {
                    System.out.println("直播配置域名!");
                    live.addLiveDomainMapping(client, liveVideo, liveEdge);
                    System.out.println("域名关联成功!");
                    break;
                }else {
                    System.out.println("域名配置中,请稍后...");
                    Thread.sleep(5000);
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            System.out.println("ErrCode:" + e.getErrCode());
            System.out.println("ErrMsg:" + e.getErrMsg());
            System.out.println("RequestId:" + e.getRequestId());
        }
    }
}

步骤二 配置直播录制

您可以通过调用AddLiveAppRecordConfig接口添加App录制配置,将录制的视频保存到阿里云OSS上。在配置视频录制时,您可以通过配置OnDemand参数选择录制方式,本教程中以手动控制录制为例。

OnDemand参数值说明如下:
  • 0:表示关闭。
  • 1:表示通过HTTP回调方式。
  • 7:表示默认不录制,可以通过RealTimeRecordCommand接口手动控制录制启停。
    说明 手动启动录制的直播流一旦发生了断流,就会停止录制。重新推流后,如果没有配置自动录制,将不会自动启动录制。

完整代码示例如下:

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.live.model.v20161101.AddLiveAppRecordConfigRequest;
import com.aliyuncs.live.model.v20161101.AddLiveAppRecordConfigResponse;
import com.aliyuncs.live.model.v20161101.RealTimeRecordCommandRequest;
import com.aliyuncs.live.model.v20161101.RealTimeRecordCommandResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.List;

/**
 * 1.配置直播录制内容保存到OSS中
 * 2.开启(关闭)直播录制
 *  说明:本示例设置的录制规则为默认不开启录制,而是通过手动调用RealTimeRecordCommand接口来控制录制的启动和停止
 */
public class AddLiveAppRecordConfig {

    private static final Gson gson = new Gson();
    // 推流域名。
    private static String streamName = "www.yourLiveEdge.club";
    // 直播流所属应用名称。支持通配符(*),代表该域名下所有的AppName。
    private static String App = "testApp";
    // 加速域名,指播放域名。
    private static String domainName = "www.yourDomainName.com";
    // OssBucket名称。
    private static String ossBucket = "oss-***-****-****";
    // OssEndpoint域名。
    private static String ossEndpoint = "oss-*****.aliyuncs.com";
    // 按需录制。
    // 0表示关闭。
    // 1表示通过HTTP回调方式。
    // 7表示默认不录制,通过RealTimeRecordCommand接口手动控制录制启停。
    private static Integer onDemand = 7;

    /**
     * Initialization  初始化公共请求参数
     */
    public IAcsClient initialization() {
        // 初始化请求参数
        DefaultProfile profile = DefaultProfile.getProfile(
                "<your-region-id>", // 您的可用区ID
                "<your-access-key-id>", // 您的AccessKey ID
                "<your-access-key-secret>"); // 您的AccessKey Secret
        return new DefaultAcsClient(profile);
    }

    /**
     * 配置APP录制,输出内容保存到OSS中
     *
     * @param client     公共请求参数
     * @param appName    直播流所属应用名称
     * @param domainName 加速域名,指播放域名
     * @param ossBucket  OssBucket名称
     * @param endpoint   OssEndpoint域名
     * @param onDemand   按需录制 0表示关闭。1表示通过HTTP回调方式。7表示默认不录制。
     * @param streamName 流名称
     */
    public void addLiveAppRecordConfig(IAcsClient client, String appName, String domainName, String ossBucket, String endpoint, Integer onDemand, String streamName, List recordFormatList) throws ClientException {
        AddLiveAppRecordConfigRequest request = new AddLiveAppRecordConfigRequest();
        System.out.println("--------------------addLiveAppRecordConfig--------------------");
        request.setAppName(appName);
        request.setDomainName(domainName);
        request.setOssBucket(ossBucket);
        request.setOssEndpoint(endpoint);
        request.setOnDemand(onDemand);
        request.setStreamName(streamName);
        request.setRecordFormats(recordFormatList);
        AddLiveAppRecordConfigResponse response = client.getAcsResponse(request);
        System.out.println(gson.toJson(response));
    }

    /**
     * 按需完成手动录制。例如,动态地启动、停止录制
     *
     * @param client     公共请求参数
     * @param appName    App名
     * @param command    操作行为。支持start、stop两种类型
     * @param domainName 您的加速域名
     * @param streamName 直播流名
     */
    public void realTimeRecordCommand(IAcsClient client, String appName, String command, String domainName, String streamName) throws ClientException {
        RealTimeRecordCommandRequest request = new RealTimeRecordCommandRequest();
        System.out.println("--------------------addLiveAppRecordConfig--------------------");
        request.setAppName(appName);
        request.setDomainName(domainName);
        request.setStreamName(streamName);
        request.setCommand(command);
        RealTimeRecordCommandResponse response = client.getAcsResponse(request);
        System.out.println(gson.toJson(response));
    }


    /**
     * 组装RecordFormat参数
     *
     * @param cycleDuration        周期录制时长。单位:秒。不填则默认为6小时。
     * @param format               格式。目前支持m3u8、flv或mp4。
     * @param ossObjectPrefix      OSS存储的录制文件名,小于256 byte,支持变量匹配
     *                             包含 {AppName}、{StreamName}、{Sequence}、{StartTime}、{EndTime}、{EscapedStartTime}、{EscapedEndTime}
     *                             参数值必须要有{StartTime}或{EscapedStartTime}和{EndTime}或{EscapedEndTime}变量。默认支持1小时周期录制,最小
     *                             周期时间15分钟,最多6小时。
     * @param sliceOssObjectPrefix 当format格式是m3u8录制,则需要配置,表示ts切片名称。默认30秒一片,小于256byte,支持变量匹配
     *                             包含{AppName}、{StreamName}、{UnixTimestamp}、{Sequence}
     */
    public List<AddLiveAppRecordConfigRequest.RecordFormat> recordFormat(Integer cycleDuration, String format, String ossObjectPrefix, String sliceOssObjectPrefix) {
        List<AddLiveAppRecordConfigRequest.RecordFormat> recordFormatList = new ArrayList<AddLiveAppRecordConfigRequest.RecordFormat>();

        AddLiveAppRecordConfigRequest.RecordFormat recordFormat1 = new AddLiveAppRecordConfigRequest.RecordFormat();
        recordFormat1.setFormat(format);
        recordFormat1.setOssObjectPrefix(ossObjectPrefix);
        recordFormat1.setCycleDuration(cycleDuration);
        recordFormat1.setSliceOssObjectPrefix(sliceOssObjectPrefix);
        recordFormatList.add(recordFormat1);
        return recordFormatList;
    }

    public static void main(String[] args) {
        AddLiveAppRecordConfig liveAppRecordConfig = new AddLiveAppRecordConfig();
        IAcsClient client = liveAppRecordConfig.initialization();
        List<AddLiveAppRecordConfigRequest.RecordFormat> recordFormats = liveAppRecordConfig.recordFormat(
                1,
                "m3u8",
                "record/{AppName}/{StreamName}/{Sequence}{EscapedStartTime}{EscapedEndTime}",
                "record/{AppName}/{StreamName}/{UnixTimestamp}_{Sequence}");
        try {
            liveAppRecordConfig.addLiveAppRecordConfig(client, App, domainName, ossBucket, ossEndpoint, 7, streamName, recordFormats);
            liveAppRecordConfig.realTimeRecordCommand(client, App, "start", domainName, streamName);
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            System.out.println("ErrCode:" + e.getErrCode());
            System.out.println("ErrMsg:" + e.getErrMsg());
            System.out.println("RequestId:" + e.getRequestId());
        }
    }
}

步骤三 配置直播鉴权

阿里云视频直播默认开启URL鉴权功能,防止直播被盗录、盗播。您可以使用默认的鉴权规则,也可以使用自定义鉴权规则。本教程基于默认鉴权规则指导您如何进行直播鉴权。

  1. 拼接推流地址。
    直播只支持RTMP格式推流。流地址格式为RTMP://推流域名/AppName/StreamName?鉴权串。
    例如,推流域名是push.aliyunlive.com,AppName为testApp,StreamName为testStream,鉴权key是123,则推流地址为RTMP://push.aliyunlive.com/app/stream?auth_key=timestamp-rand-uid-md5hash
    说明 计算md5的鉴权值规则是:/appname/streamname-unix时间戳-0-0-鉴权KEY -> md5sum 得到鉴权值 。

    例如:/testApp/testStream-1568979058-0-0-u61XUjAZiM -> md5后等于 ed8e6df060d103b6cbfb10359a2cf089

  2. 拼接播流地址。
    播流地址支持RMTP、FLV、HLS格式。格式如下所示:
    • RTMP:rtmp://播流域名/AppName/StreamName?鉴权串
    • FLV:http://播流域名/AppName/StreamName.flv?鉴权串
    • HLS:http://播流域名/AppName/StreamName.m3u8?鉴权串
      说明 M3u8转码地址已支持。如果您有需要,请您 提交工单 申请。
    例如:播流域名是play.aliyunlive.com,AppName为app,StreamName为stream,鉴权key是 456,则播流地址为:
    • RTMP:rtmp://play.aliyunlive.com/app/stream?auth_key=timestamp-rand-uid-md5hash
    • FLV:http://play.aliyunlive.com/app/stream.flv?auth_key=timestamp-rand-uid-md5hash
    • HLS:/http://play.aliyunlive.com/app/stream.m3u8?auth_key=timestamp-rand-uid-md5hash

完整代码示例如下:

import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import java.util.HashMap;
import java.util.Map;

/**
 * 推拉流地址示例:
 * rtmp://www.ttest.liveTest.com/a/a?auth_key=1558065152-0-0-*****
 * 播流地址
 * 原画
 * rtmp://www.btest.liveTest.com/a/a?auth_key=1558065152-0-0-*****
 * http://www.btest.liveTest.com/a/a.flv?auth_key=1558065152-0-0-*****
 * http://www.btest.liveTest.com/a/a.m3u8?auth_key=1558065152-0-0-*****
 * 
 * hutool工具包地址,请参见https://hutool.cn/docs/#/
 */
public class AliyunLiveUtil {

    // 鉴权url的有效时间(秒),默认30分钟,1800秒
    private static Integer identUrlValidTime = 1800;
    // 直播测试appName
    private static String appName = "testApp";
    // 直播测试streamName
    private static String streamName = "testStranm";
    // 推流域名
    private static String pushDomain = "www.yourLiveEdge.club";
    // 推流鉴权url
    private static String pushIdentKey = "******";
    // 拉流域名
    private static String pullDomain = "www.yourLiveVideo.club";
    // 拉流鉴权url
    private static String pullIdentKey = "******";

    /**
     * 根据源id创建该id的推流url
     *
     * @param identUrlValidTime 鉴权url的有效时间(秒),默认30分钟,1800秒
     * @param pushDomain        推流域名
     * @param appName           直播测试appName
     * @param streamName        直播测试streamName
     * @param pushIdentKey      推流鉴权url key
     * @return
     */
    public String createPushUrl(Integer identUrlValidTime, String pushDomain, String appName, String streamName, String pushIdentKey) {

        // 计算过期时间
        String timestamp = String.valueOf((System.currentTimeMillis() / 1000) + identUrlValidTime);

        // 组合推流域名前缀
        // rtmp://{pushDomain}/{appName}/{streamName}
        String rtmpUrl = StrUtil.format("rtmp://{}/{}/{}", pushDomain, appName, streamName);
        System.out.println("推流域名前缀,rtmpUrl=" + rtmpUrl);
        // 组合md5加密串
        // /{appName}/{streamName}-{timestamp}-0-0-{pushIdentKey}
        String md5Url = StrUtil.format("/{}/{}-{}-0-0-{}", appName, streamName, timestamp, pushIdentKey);

        // md5加密
        String md5Str = DigestUtil.md5Hex(md5Url);
        System.out.println("md5加密串,md5Url=" + md5Url + "------md5加密结果,md5Str=" + md5Str);

        // 组合最终鉴权过的推流域名
        // {rtmpUrl}?auth_key={timestamp}-0-0-{md5Str}
        String finallyPushUrl = StrUtil.format("{}?auth_key={}-0-0-{}", rtmpUrl, timestamp, md5Str);
        System.out.println("最终鉴权过的推流域名=" + finallyPushUrl);

        return finallyPushUrl;
    }

    /**
     * 创建拉流域名,key=rtmpUrl、flvUrl、m3u8Url,代表三种拉流类型域名
     *
     * @param pullDomain        拉流域名
     * @param appName           应用名称
     * @param streamName        流名称
     * @param pullIdentKey      拉流鉴权url key
     * @param identUrlValidTime 鉴权url的有效时间(秒),默认30分钟,1800秒
     * @return
     */
    public Map<String, String> createPullUrl(String pullDomain, String appName, String streamName, String pullIdentKey, Integer identUrlValidTime) {

        // 计算过期时间
        String timestamp = String.valueOf((System.currentTimeMillis() / 1000) + identUrlValidTime);

        // 组合通用域名
        // {pullDomain}/{appName}/{streamName}
        String pullUrl = StrUtil.format("{}/{}/{}", pullDomain, appName, streamName);
        System.out.println("组合通用域名,pullUrl=" + pullUrl);

        // 组合md5加密串
        // /{appName}/{streamName}-{timestamp}-0-0-{pullIdentKey}
        String md5Url = StrUtil.format("/{}/{}-{}-0-0-{}", appName, streamName, timestamp, pullIdentKey);
        String md5FlvUrl = StrUtil.format("/{}/{}.flv-{}-0-0-{}", appName, streamName, timestamp, pullIdentKey);
        String md5M3u8Url = StrUtil.format("/{}/{}.m3u8-{}-0-0-{}", appName, streamName, timestamp, pullIdentKey);

        // md5加密
        String md5Str = DigestUtil.md5Hex(md5Url);
        String md5FlvStr = DigestUtil.md5Hex(md5FlvUrl);
        String md5M3u8Str = DigestUtil.md5Hex(md5M3u8Url);
        System.out.println("md5加密串,md5Url    =" + md5Url + "       ------     md5加密结果,md5Str=" + md5Str);
        System.out.println("md5加密串,md5FlvUrl =" + md5FlvUrl + "    ------    md5加密结果,md5FlvStr=" + md5FlvStr);
        System.out.println("md5加密串,md5M3u8Url=" + md5M3u8Url + "   ------    md5加密结果,md5M3u8Str=" + md5M3u8Str);

        // 组合三种拉流域名前缀
        // rtmp://{pullUrl}?auth_key={timestamp}-0-0-{md5Str}
        String rtmpUrl = StrUtil.format("rtmp://{}?auth_key={}-0-0-{}", pullUrl, timestamp, md5Str);
        // http://{pullUrl}.flv?auth_key={timestamp}-0-0-{md5FlvStr}
        String flvUrl = StrUtil.format("http://{}.flv?auth_key={}-0-0-{}", pullUrl, timestamp, md5FlvStr);
        // http://{pullUrl}.m3u8?auth_key={timestamp}-0-0-{md5M3u8Str}
        String m3u8Url = StrUtil.format("http://{}.m3u8?auth_key={}-0-0-{}", pullUrl, timestamp, md5M3u8Str);

        System.out.println("最终鉴权过的拉流rtmp域名=" + rtmpUrl);
        System.out.println("最终鉴权过的拉流flv域名 =" + flvUrl);
        System.out.println("最终鉴权过的拉流m3u8域名=" + m3u8Url);

        HashMap<String, String> urlMap = new HashMap<String, String>();
        urlMap.put("rtmpUrl", rtmpUrl);
        urlMap.put("flvUrl", flvUrl);
        urlMap.put("m3u8Url", m3u8Url);

        return urlMap;
    }

    public static void main(String[] args) {
        AliyunLiveUtil aliyunLiveUtil = new AliyunLiveUtil();
        aliyunLiveUtil.createPushUrl(identUrlValidTime, pushDomain, appName, streamName, pushIdentKey);
        aliyunLiveUtil.createPullUrl(pullDomain, appName, streamName, pullIdentKey, identUrlValidTime);
    }
}            

步骤四 开始直播

在完成直播鉴权之后,您就可以使用鉴权后的推流地址进行直播推流了。

  1. 下载并安装OBS推流工具。

    关于OBS播放器的使用,请参见OBS推流工具

  2. 推流配置中输入服务器地址和串流密匙。
    • 服务器:填写包含AppName前的地址。
    • 串流密匙:填写包含StreamName后的地址。
    例如,推流地址为rtmp://push.aliyunlive.com/testApp/testStream?auth_key=1543302081-0-0-9c6e7c8190c10bdfb3c0************,则服务器地址为rtmp://push.aliyunlive.com/testApp/串流密匙testStream?auth_key=1543302081-0-0-9c6e7c8190c10bdfb3c0************
    说明 以上推流地址示例由推流域名、AppName、StreamName和鉴权串组成,您需要根据实际情况,替换成您自己的AppName、StreamName和相应的鉴权串。
  3. 完成以下操作进行播流:
    1. 下载并安装VLC播放器。

      关于VLC播放器的使用,请参见VLC播放器

    2. 打开VLC播放器,然后选择媒体 > 打开网络串流(N)
    3. 请输入网络URL 中,输入播流地址并单击播放