雲計算

OSS 直播功能深度分享

作者:張醫博

場景描述

目前有很多直播愛好者使用的是 OSS + RTMP 做的推流直播,其中不乏一些企業級別應用,由於 OSS 作為推流的接收略微有一些複雜,故單獨開篇講一下。其實不建議使用 OSS+RTMP 做直播推流,因為功能性相比專業的阿里雲直播產品來說,OSS 的推流適合監管備份等特定場景 ,客戶端對直播推流的延遲要求不是很敏感。如果對直播的拉流推流延遲有高敏感的場景,建議大家使用阿里雲視頻直播服務,可以做到上下行鏈路加速,且支持多樣化的直播功能適配;

使用基礎

簡單的直播知識

您需要了解甚至掌握簡單的直播知識技巧才能熟練的使用 OSS 直播,不僅是針對 OSS 還涉及到客戶端的推流等相關問題,所以需要明白直播的相關基礎

【音視頻頭介紹】

OSS 的直播功能是建立在 RTMP 直播傳輸協議的基礎上,所以需要指導一些基礎的 RMTP 知識:RTMP 的音視頻流的封裝形式和 FLV 格式相似, 流媒體服務器向客戶端發送包含 H264 和 AAC 的 RTMP 直播流,需要首先發送這兩個 header,沒有這些信息播放端是無法解碼音視頻流的,其中音頻 tag 格式如下

1)AVC sequence header
2)AAC sequence header
image
從上面推論出 AAC sequence header 內容的前 2 個字節是 0xAF 0x00,我們來看一個示例:
image

3)ADIF:Audio Data Interchange Format 音頻數據交換格式。這種格式的特徵是可以確定的找到這個音頻數據的開始,不需進行在音頻數據流中間開始的解碼,即它的解碼必須在明確定義的開始處進行。故這種格式常用在磁盤文件中。

4)ADTS:Audio Data Transport Stream 音頻數據傳輸流。這種格式的特徵是它是一個有同步字的比特流,解碼可以在這個流中任何位置開始。它的特徵類似於 mp3 數據流格式。

【RTMP 內容介紹】

以下是我對 RTMP 總結的一張完整描述圖

image

對象存儲推流架構

看下官網對於 OSS 推流的過程定義:

1)只能使用RTMP推流的方式,不支持拉流。
2)必須包含視頻流,且視頻流格式為H264。
3)音頻流是可選的,並且只支持AAC格式,其他格式的音頻流會被丟棄。
4)轉儲只支持HLS協議。
5)一個LiveChannel同時只能有一個客戶端向其推流。

RTMP 的推流格式:

  • demo :rtmp://your-bucket.oss-cn-- hangzhou.aliyuncs.com/live/test-channel
  • live 等同於 RTMP 的 APP 掛載點
  • test-channel 等同於 RTMP 的 stream name

RTMP URL 推流簽名:

  • demo:
    rtmp://${bucket}.${host}/live/${channel}?OSSAccessKeyId=xxx&Expires=yyy&Signature=zzz&${params}
  • 推流前 LiveChannel有兩種Status:enabled和disabled,用戶可以使用本接口在兩種Status之間進行切換。處於disabled狀態時,OSS會禁止用戶向該LiveChannel進行推流操作;如果有用戶正在向該LiveChannel推流,那麼推流的客戶端會被強制斷開(可能會有10s左右的延遲)

對象存儲推流的流程彙總圖如下

生成推流 URL API
設置推流狀態API

image

使用 JAVA SDK 生成推流地址

我們現在用 java 的 SDK 演示一下如上的推理過程,在跑 SDK 之前,需要先搭建好一套本地的 eclipse 環境,如下是我用的 eclipse,如果有沒搭建請網上搜索一下 eclipse 的搭建方式(之前需要安裝 JDK 且配置環境變量)

環境要求

  • Eclipse 版本:Version: Neon.3 Release (4.6.3)
  • JDK 版本:jdk1.8.0_144
  • OSS:公開讀(為了驗證推流功能是否正常,我們用公開讀的方式快速測試)
  • 我們採用主函數入口的方式,實例化其他類進行調用,這樣方便類的拆分,不用都集合在主函數中。
  • 主函數 domain,實例化 OSSClient 對象傳入到 RtmpTest 類中測試。
  • 所有的 jar 都會通過官方的 maven 解決的依賴關係,https://help.aliyun.com/document_detail/32009.html
package javasdk;

import java.io.FileNotFoundException;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

import com.aliyun.oss.OSSClient;

public class domain {
    public static void main( String[] args ) throws ParseException, FileNotFoundException
    {
        
        System.out.println( "Hello World!" );
        String accessid = "AK";
        String secretkey = "SK";
        String objectpath = "C://Users//hanli.zyb//Desktop//running.png";
        String bucket = "bucket";
        String object = "running";
        String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";

        
        // OSS + rtmp 推流
        String bucketName = "ali-hangzhou";
        RtmpTest pushoss = new RtmpTest();
        OSSClient ossClient = new OSSClient(endpoint, AK, SK);
        pushoss.testCreateLiveChannel(bucketName,ossClient);

    }
}

package javasdk;

import java.text.ParseException;
import java.util.Date;
import java.util.List;
import junit.framework.Assert;

import org.junit.Ignore;
import org.junit.Test;

import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSErrorCode;
import com.aliyun.oss.OSSException;
import com.aliyun.oss.common.utils.DateUtil;
import com.aliyun.oss.model.CannedAccessControlList;
import com.aliyun.oss.model.CreateLiveChannelRequest;
import com.aliyun.oss.model.CreateLiveChannelResult;
import com.aliyun.oss.model.ListLiveChannelsRequest;
import com.aliyun.oss.model.LiveChannel;
import com.aliyun.oss.model.LiveChannelInfo;
import com.aliyun.oss.model.LiveChannelListing;
import com.aliyun.oss.model.LiveChannelStat;
import com.aliyun.oss.model.LiveChannelStatus;
import com.aliyun.oss.model.LiveChannelTarget;
import com.aliyun.oss.model.LiveRecord;
import com.aliyun.oss.model.PushflowStatus;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;

public class RtmpTest  {
    String bucketName = "bucket";
    final String liveChannel = "stream name";
    @Test
    public void testCreateLiveChannelDefault(String bucketname,OSSClient ossClient) {

        try {
            CreateLiveChannelRequest createLiveChannelRequest = new CreateLiveChannelRequest(
                    bucketName, liveChannel);
            CreateLiveChannelResult createLiveChannelResult = ossClient.createLiveChannel(createLiveChannelRequest);
            LiveChannelInfo liveChannelInfo = ossClient.getLiveChannelInfo(bucketName, liveChannel);

            
            ossClient.deleteLiveChannel(bucketName, liveChannel);
        } catch (Exception e) {
            Assert.fail(e.getMessage());
        }
    }
    
    @Test
    public void testCreateLiveChannel(String bucketname,OSSClient ossClient) {
        final String liveChannel = "normal-create-live-channel";
        final String liveChannelDesc = "my test live channel";

        try {
            LiveChannelTarget target = new LiveChannelTarget("HLS", 100, 99, "myplaylist.m3u8");
            CreateLiveChannelRequest createLiveChannelRequest = new CreateLiveChannelRequest(
                    bucketName, liveChannel, liveChannelDesc, LiveChannelStatus.Enabled, target);
            
            CreateLiveChannelResult createLiveChannelResult = ossClient.createLiveChannel(createLiveChannelRequest);
            System.out.println(createLiveChannelResult.getPublishUrls());
            /*Assert.assertEquals(createLiveChannelResult.getPublishUrls().size(), 1);
            Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).startsWith("rtmp://"));
            Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).endsWith("live/" + liveChannel));
            Assert.assertEquals(createLiveChannelResult.getPlayUrls().size(), 1);
            Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).startsWith("http://"));
            Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).endsWith(liveChannel + "/myplaylist.m3u8"));*/
            
           /* LiveChannelInfo liveChannelInfo = ossClient.getLiveChannelInfo(bucketName, liveChannel);
            Assert.assertEquals(liveChannelInfo.getDescription(), liveChannelDesc);
            Assert.assertEquals(liveChannelInfo.getStatus(), LiveChannelStatus.Disabled);
            Assert.assertEquals(liveChannelInfo.getTarget().getType(), "HLS");
            Assert.assertEquals(liveChannelInfo.getTarget().getFragDuration(), 100);
            Assert.assertEquals(liveChannelInfo.getTarget().getFragCount(), 99);
            Assert.assertEquals(liveChannelInfo.getTarget().getPlaylistName(), "myplaylist.m3u8");*/
            
            
           // ossClient.deleteLiveChannel(bucketName, liveChannel);
        } catch (Exception e) {
            Assert.fail(e.getMessage());
        }
    }
}

其中我們最關注的是 testCreateLiveChannel 類,創建了推流地址,其中參數 LiveChannelStatus.enable 是要讓推流變為可用狀態。
running 主函數,打印出推流地址。
rtmp://hangzhou.oss-cn-hangzhou.aliyuncs.com/live/normal-create-live-channel

public void testCreateLiveChannel(String bucketname,OSSClient ossClient) {
        final String liveChannel = "normal-create-live-channel";
        final String liveChannelDesc = "my test live channel";

        try {
            LiveChannelTarget target = new LiveChannelTarget("HLS", 100, 99, "myplaylist.m3u8");
            CreateLiveChannelRequest createLiveChannelRequest = new CreateLiveChannelRequest(
                    bucketName, liveChannel, liveChannelDesc, LiveChannelStatus.Enabled, target);
            
            CreateLiveChannelResult createLiveChannelResult = ossClient.createLiveChannel(createLiveChannelRequest);
            System.out.println(createLiveChannelResult.getPublishUrls());
            /*Assert.assertEquals(createLiveChannelResult.getPublishUrls().size(), 1);
            Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).startsWith("rtmp://"));
            Assert.assertTrue(createLiveChannelResult.getPublishUrls().get(0).endsWith("live/" + liveChannel));
            Assert.assertEquals(createLiveChannelResult.getPlayUrls().size(), 1);
            Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).startsWith("http://"));
            Assert.assertTrue(createLiveChannelResult.getPlayUrls().get(0).endsWith(liveChannel + "/myplaylist.m3u8"));*/
            
            
           // ossClient.deleteLiveChannel(bucketName, liveChannel);
        } catch (Exception e) {
            Assert.fail(e.getMessage());
        }
    }​

Leave a Reply

Your email address will not be published. Required fields are marked *