Android 端使用七牛霹雳直播云服务

Android 端使用七牛霹雳直播云服务实现直播。

# 七牛服务器配置

七牛直播云服务 (opens new window)创建一个直播空间,按需配置。

PILI 直播云服务 (opens new window)创建配置上面创建的直播空间,由于 Android SDK 1.0 不支持动态鉴权,这里先配置为静态鉴权。

# SDK 安装

官网 (opens new window)下载 SDK 压缩包,PLDroidMediaStreamingPLDroidPlayer,解压后可分别见4个文件夹 arm64-v8a armeabi armeabi-v7a x86 和一个 jar 文件。

把 jar 文件复制到 app/libs

把4个文件夹复制到 app/src/main/java/jniLibs

添加其他依赖

compile 'com.qiniu:happy-dns:0.2.7'
compile 'com.qiniu.pili:pili-android-qos:0.8.20'

配置权限

<uses-feature android:glEsVersion="0x00020000"
              android:required="true" />
<uses-feature android:name="android.hardware.camera.autofocus" />

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
# 推流 (opens new window)
public class SWCameraStreamingActivity extends Activity
        implements StreamingStateChangedListener {
    private JSONObject mJSONObject;
    private MediaStreamingManager mCameraStreamingManager;
    private StreamingProfile mProfile;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera_streaming);

        AspectFrameLayout afl = (AspectFrameLayout) findViewById(R.id.cameraPreview_afl);
        afl.setShowMode(AspectFrameLayout.SHOW_MODE.REAL);
        GLSurfaceView glSurfaceView = (GLSurfaceView) findViewById(R.id.cameraPreview_surfaceView);

        String publishUrl = getIntent().getStringExtra("publish_url");
        try {
            mProfile = new StreamingProfile();
            mProfile.setVideoQuality(StreamingProfile.VIDEO_QUALITY_HIGH1)
                    .setAudioQuality(StreamingProfile.AUDIO_QUALITY_MEDIUM2)
                    .setEncodingSizeLevel(StreamingProfile.VIDEO_ENCODING_HEIGHT_480)
                    .setEncoderRCMode(StreamingProfile.EncoderRCModes.QUALITY_PRIORITY)
                    .setPublishUrl(publishUrl);

            CameraStreamingSetting setting = new CameraStreamingSetting();
            setting.setCameraId(Camera.CameraInfo.CAMERA_FACING_BACK)
                    .setContinuousFocusModeEnabled(true)
                    .setCameraPrvSizeLevel(CameraStreamingSetting.PREVIEW_SIZE_LEVEL.MEDIUM)
                    .setCameraPrvSizeRatio(CameraStreamingSetting.PREVIEW_SIZE_RATIO.RATIO_16_9);

            mCameraStreamingManager = new MediaStreamingManager(this, afl, glSurfaceView,
                    AVCodecType.SW_VIDEO_WITH_SW_AUDIO_CODEC);

            mCameraStreamingManager.prepare(setting, mProfile);
            mCameraStreamingManager.setStreamingStateListener(this);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        mCameraStreamingManager.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        // You must invoke pause here.
        mCameraStreamingManager.pause();
    }

    @Override
    public void onStateChanged(StreamingState state, Object info) {
        switch (state) {
            case PREPARING:
                break;
            case READY:
                // start streaming when READY
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        if (mCameraStreamingManager != null) {
                            mCameraStreamingManager.startStreaming();
                        }
                    }
                }).start();
                break;
            case CONNECTING:
                break;
            case STREAMING:
                // The av packet had been sent.
                break;
            case SHUTDOWN:
                // The streaming had been finished.
                break;
            case IOERROR:
                // Network connect error.
                break;
            case SENDING_BUFFER_EMPTY:
                break;
            case SENDING_BUFFER_FULL:
                break;
            case AUDIO_RECORDING_FAIL:
                // Failed to record audio.
                break;
            case OPEN_CAMERA_FAIL:
                // Failed to open camera.
                break;
            case DISCONNECTED:
                // The socket is broken while streaming
                break;
        }
    }
}
# 播放 (opens new window)
PLVideoView vv = (PLVideoView) findViewById(R.id.PLVideoView);
vv.setVideoPath("rtmp://pili-live-rtmp.sb.live.mothin.com/mothin/st2");
vv.start();
 <com.pili.pldroid.player.widget.PLVideoView
                                             .../>

应用服务器

const express = require('express');
const pili = require('piliv2');
const config = require('../config');
const hash = require('../util/hash');

const router = express.Router();
const credentials = new pili.Credentials(config.qiniu.accessKey, config.qiniu.secretKey);
const HUB = 'mothin';
pili.config.API_HOST = 'pili.qiniuapi.com';
const publishDomain = 'pili-publish.sb.live.mothin.com';

const hub = new pili.Hub(credentials, HUB);

router.get('/publish-url/:stream', function (req, res, next) {
  const publishKey = hub.newStream(req.params.stream).credentials.publishKey;
  res.send(publishURL(publishDomain, HUB, req.params.stream, 60, publishKey));
});

router.get('/new-stream/:stream', function (req, res, next) {
  res.send(hub.newStream(req.params.stream));
});

module.exports = router;

function publishURL(domain, hub, key, expireAfterSec, publishKey) {
  var expire = Math.floor(Date.now() / 1000) + expireAfterSec;
  var path = `/${hub}/${key}?expire=${expire}`
  var token = sign(path, publishKey);
  return `rtmp://${domain}${path}&token=${token}`;
}

function sign(data, publishKey) {
  var digest = hash.hmacSha1(data, publishKey);
  var sageDigest = hash.base64ToUrlSafe(digest);
  return sageDigest;
}

// var pu = Pili.publishURL(credentials, 'publish-rtmp.sb.live.mothin.com', HUB, 'streamkey', 60);
// var rtmpURL = Pili.rtmpPlayURL('live-rtmp.sb.live.mothin.com', HUB, 'streamkey');
// var hdlURL = Pili.hdlPlayURL('live-rtmp.sb.live.mothin.com', HUB, 'streamkey');
// var hlsURL = Pili.hlsPlayURL('live-rtmp.sb.live.mothin.com', HUB, 'streamkey');
// var snapURL = Pili.snapshotPlayURL('live-rtmp.sb.live.mothin.com', HUB, 'streamkey');