视频直播服务(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>

步骤一:添加视频直播域名

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

完整代码示例如下:

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 = "example.com";
    // 播流域名
    private static String liveVideo = "example.com";

    /**
     * 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     需要接入直播的域名。支持泛域名,以符号“.”开头,如:.example.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 = "example.com";
    // 直播流所属应用名称。支持通配符(*),代表该域名下所有的AppName。
    private static String App = "testApp";
    // 加速域名,指播放域名。
    private static String domainName = "example.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?鉴权串。
    例如,推流域名是example.com,AppName为testApp,StreamName为testStream,鉴权key是123,则推流地址为RTMP://example.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转码地址已支持。如果您有需要,请您 提交工单 申请。
    例如:播流域名是example.com,AppName为app,StreamName为stream,鉴权key是 456,则播流地址为:
    • RTMP:rtmp://example.com/app/stream?auth_key=timestamp-rand-uid-md5hash
    • FLV:http://example.com/app/stream.flv?auth_key=timestamp-rand-uid-md5hash
    • HLS:/http://example.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.example.com/a/a?auth_key=1558065152-0-0-*****
 * 播流地址
 * 原画
 * rtmp://www.example.com/a/a?auth_key=1558065152-0-0-*****
 * http://www.example.com/a/a.flv?auth_key=1558065152-0-0-*****
 * http://www.example.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 = "example.com";
    // 推流鉴权url
    private static String pushIdentKey = "******";
    // 拉流域名
    private static String pullDomain = "example.com";
    // 拉流鉴权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://example.com/testApp/testStream?auth_key=1543302081-0-0-9c6e7c8190c10bdfb3c0************,则服务器地址为rtmp://example.com/testApp/串流密钥testStream?auth_key=1543302081-0-0-9c6e7c8190c10bdfb3c0************
    说明 以上推流地址示例由推流域名、AppName、StreamName和鉴权串组成,您需要根据实际情况,替换成您自己的AppName、StreamName和相应的鉴权串。
  3. 完成以下操作进行播流:
    1. 下载并安装VLC播放器。

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

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