diff --git a/phpunit.xml b/phpunit.xml index 41a11a35..bce8cb77 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -11,6 +11,7 @@ ./tests ./tests/OSS/Tests/BucketCnameTest.php + ./tests/OSS/Tests/BucketLiveChannelTest.php diff --git a/src/OSS/Model/LiveChannelConfig.php b/src/OSS/Model/LiveChannelConfig.php new file mode 100644 index 00000000..0c4aa56c --- /dev/null +++ b/src/OSS/Model/LiveChannelConfig.php @@ -0,0 +1,135 @@ +id = $option['id']; + } + if (isset($option['description'])) { + $this->description = $option['description']; + } + if (isset($option['status'])) { + $this->status = $option['status']; + } + if (isset($option['type'])) { + $this->type = $option['type']; + } + if (isset($option['fragDuration'])) { + $this->fragDuration = $option['fragDuration']; + } + if (isset($option['fragCount'])) { + $this->fragCount = $option['fragCount']; + } + if (isset($option['playListName'])) { + $this->playListName = $option['playListName']; + } + } + + public function getId() + { + return $this->id; + } + + public function getDescription() + { + return $this->description; + } + + public function getStatus() + { + return $this->status; + } + + public function getType() + { + return $this->type; + } + + public function getFragDuration() + { + return $this->fragDuration; + } + + public function getFragCount() + { + return $this->fragCount; + } + + public function getPlayListName() + { + return $this->playListName; + } + + public function parseFromXml($strXml) + { + $xml = simplexml_load_string($strXml); + $this->description = strval($xml->Description); + $this->status = strval($xml->Status); + $target = $xml->Target; + $this->type = strval($target->Type); + $this->fragDuration = intval($target->FragDuration); + $this->fragCount = intval($target->FragCount); + $this->playListName = strval($target->PlayListName); + } + + public function serializeToXml() + { + $strXml = << + + +EOF; + $xml = new \SimpleXMLElement($strXml); + if (isset($this->description)) { + $xml->addChild('Description', $this->description); + } + + if (isset($this->status)) { + $xml->addChild('Status', $this->status); + } + + $node = $xml->addChild('Target'); + $node->addChild('Type', $this->type); + + if (isset($this->fragDuration)) { + $node->addChild('FragDuration', $this->fragDuration); + } + + if (isset($this->fragCount)) { + $node->addChild('FragCount', $this->fragCount); + } + + if (isset($this->playListName)) { + $node->addChild('PlayListName', $this->playListName); + } + + return $xml->asXML(); + } + + public function __toString() + { + return $this->serializeToXml(); + } +} diff --git a/src/OSS/Model/LiveChannelInfo.php b/src/OSS/Model/LiveChannelInfo.php new file mode 100644 index 00000000..e5642678 --- /dev/null +++ b/src/OSS/Model/LiveChannelInfo.php @@ -0,0 +1,107 @@ +id = $id; + $this->description = $description; + $this->publishUrls = array(); + $this->playUrls = array(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getPublishUrls() + { + return $this->publishUrls; + } + + public function getPlayUrls() + { + return $this->playUrls; + } + + public function getStatus() + { + return $this->status; + } + + public function getLastModified() + { + return $this->lastModified; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function parseFromXmlNode($xml) + { + if (isset($xml->Id)) { + $this->id = strval($xml->Id); + } + + if (isset($xml->Description)) { + $this->description = strval($xml->Description); + } + + if (isset($xml->Status)) { + $this->status = strval($xml->Status); + } + + if (isset($xml->LastModified)) { + $this->lastModified = strval($xml->LastModified); + } + + if (isset($xml->PublishUrls)) { + foreach ($xml->PublishUrls as $url) { + $this->publishUrls[] = strval($url->Url); + } + } + + if (isset($xml->PlayUrls)) { + foreach ($xml->PlayUrls as $url) { + $this->playUrls[] = strval($url->Url); + } + } + } + + public function parseFromXml($strXml) + { + $xml = simplexml_load_string($strXml); + $this->parseFromXmlNode($xml); + } + + public function serializeToXml() + { + throw new OssException("Not implemented."); + } +} diff --git a/src/OSS/Model/LiveChannelListInfo.php b/src/OSS/Model/LiveChannelListInfo.php new file mode 100644 index 00000000..6be16e4e --- /dev/null +++ b/src/OSS/Model/LiveChannelListInfo.php @@ -0,0 +1,108 @@ +bucketName; + } + + public function setBucketName($name) + { + $this->bucketName = $name; + } + + /** + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * @return string + */ + public function getMarker() + { + return $this->marker; + } + + /** + * @return int + */ + public function getMaxKeys() + { + return $this->maxKeys; + } + + /** + * @return mixed + */ + public function getIsTruncated() + { + return $this->isTruncated; + } + + /** + * @return LiveChannelInfo[] + */ + public function getChannelList() + { + return $this->channelList; + } + + /** + * @return string + */ + public function getNextMarker() + { + return $this->nextMarker; + } + + public function parseFromXml($strXml) + { + $xml = simplexml_load_string($strXml); + + $this->prefix = strval($xml->Prefix); + $this->marker = strval($xml->Marker); + $this->maxKeys = intval($xml->MaxKeys); + $this->isTruncated = (strval($xml->IsTruncated) == 'true'); + $this->nextMarker = strval($xml->NextMarker); + + if (isset($xml->LiveChannel)) { + foreach ($xml->LiveChannel as $chan) { + $channel = new LiveChannelInfo(); + $channel->parseFromXmlNode($chan); + $this->channelList[] = $channel; + } + } + } + + public function serializeToXml() + { + throw new OssException("Not implemented."); + } +} \ No newline at end of file diff --git a/src/OSS/OssClient.php b/src/OSS/OssClient.php index 982297bd..8c86f50e 100644 --- a/src/OSS/OssClient.php +++ b/src/OSS/OssClient.php @@ -9,6 +9,9 @@ use OSS\Model\CorsConfig; use OSS\Model\CnameConfig; use OSS\Model\LoggingConfig; +use OSS\Model\LiveChannelConfig; +use OSS\Model\LiveChannelInfo; +use OSS\Model\LiveChannelListInfo; use OSS\Result\AclResult; use OSS\Result\BodyResult; use OSS\Result\GetCorsResult; @@ -26,6 +29,8 @@ use OSS\Result\ListPartsResult; use OSS\Result\PutSetDeleteResult; use OSS\Result\ExistResult; +use OSS\Result\PutLiveChannelResult; +use OSS\Result\ListLiveChannelResult; use OSS\Model\ObjectListInfo; use OSS\Result\UploadPartResult; use OSS\Model\BucketListInfo; @@ -513,6 +518,119 @@ public function deleteBucketCname($bucket, $cname, $options = NULL) return $result->getData(); } + /** + * 为指定Bucket创建直播流 + * + * @param string $bucket bucket名称 + * @param LiveChannelConfig $channelConfig + * @param array $options + * @throws OssException + * @return LiveChannelInfo + */ + public function putBucketLiveChannel($bucket, $channelConfig, $options = NULL) + { + $this->precheckCommon($bucket, NULL, $options, false); + $options[self::OSS_BUCKET] = $bucket; + $options[self::OSS_METHOD] = self::OSS_HTTP_PUT; + $options[self::OSS_OBJECT] = $channelConfig->getId(); + $options[self::OSS_SUB_RESOURCE] = 'live'; + $options[self::OSS_CONTENT_TYPE] = 'application/xml'; + $options[self::OSS_CONTENT] = $channelConfig->serializeToXml(); + + $response = $this->auth($options); + $result = new PutLiveChannelResult($response); + $info = $result->getData(); + $info->setId($channelConfig->getId()); + $info->setDescription($channelConfig->getDescription()); + + return $info; + } + + /** + * 获取指定Bucket的直播流列表 + * + * @param string $bucket bucket名称 + * @param array $options + * @throws OssException + * @return LiveChannelListInfo + */ + public function listBucketLiveChannels($bucket, $options = NULL) + { + $this->precheckCommon($bucket, NULL, $options, false); + $options[self::OSS_BUCKET] = $bucket; + $options[self::OSS_METHOD] = self::OSS_HTTP_GET; + $options[self::OSS_OBJECT] = '/'; + $options[self::OSS_SUB_RESOURCE] = 'live'; + $options[self::OSS_QUERY_STRING] = array( + 'prefix' => isset($options['prefix']) ? $options['prefix'] : '', + 'marker' => isset($options['marker']) ? $options['marker'] : '', + 'max-keys' => isset($options['max-keys']) ? $options['max-keys'] : '', + ); + $response = $this->auth($options); + $result = new ListLiveChannelResult($response); + $list = $result->getData(); + $list->setBucketName($bucket); + + return $list; + } + + /** + * 删除指定Bucket的直播流 + * + * @param string $bucket bucket名称 + * @param string $channelId + * @param array $options + * @throws OssException + * @return null + */ + public function deleteBucketLiveChannel($bucket, $channelId, $options = NULL) + { + $this->precheckCommon($bucket, NULL, $options, false); + $options[self::OSS_BUCKET] = $bucket; + $options[self::OSS_METHOD] = self::OSS_HTTP_DELETE; + $options[self::OSS_OBJECT] = $channelId; + $options[self::OSS_SUB_RESOURCE] = 'live'; + + $response = $this->auth($options); + $result = new PutSetDeleteResult($response); + return $result->getData(); + } + + /** + * 生成签名后的推流地址 + * + * @param string $bucket bucket名称 + * @param string $channelId + * @param array $options + * @throws OssException + * @return null + */ + public function getLiveChannelUrl($bucket, $channelId, $options = NULL) + { + $this->precheckCommon($bucket, $channelId, $options, false); + $expires = isset($options['expires']) ? intval($options['expires']) : 3600; + $expires = time() + $expires; + $proto = 'rtmp://'; + $hostname = $this->generateHostname($bucket); + $cano_params = ''; + $query_items = array(); + $params = isset($options['params']) ? $options['params'] : array(); + uksort($params, 'strnatcasecmp'); + foreach ($params as $key => $value) { + $cano_params = $cano_params . $key . ':' . $value . '\n'; + $query_items[] = rawurlencode($key) . '=' . rawurlencode($value); + } + $resource = '/' . $bucket . '/' . $channelId; + + $string_to_sign = $expires . '\n' . $cano_params . $resource; + $signature = base64_encode(hash_hmac('sha1', $string_to_sign, $this->accessKeySecret, true)); + $query_items[] = 'AccessKeyId=' . rawurlencode($this->accessKeyId); + $query_items[] = 'Expires=' . rawurlencode($expires); + $query_items[] = 'Signature=' . rawurlencode($signature); + + return $proto . $hostname . '/live/' . $channelId . '?' . implode('&', $query_items); + } + /** * 检验跨域资源请求, 发送跨域请求之前会发送一个preflight请求(OPTIONS)并带上特定的来源域, * HTTP方法和header信息等给OSS以决定是否发送真正的请求。 OSS可以通过putBucketCors接口 @@ -1521,7 +1639,7 @@ private function auth($options) // 获得当次请求使用的协议头,是https还是http $scheme = $this->useSSL ? 'https://' : 'http://'; // 获得当次请求使用的hostname,如果是公共域名或者专有域名,bucket拼在前面构成三级域名 - $hostname = $this->generateHostname($options); + $hostname = $this->generateHostname($options[self::OSS_BUCKET]); $string_to_sign = ''; $headers = $this->generateHeaders($options, $hostname); $signable_query_string_params = $this->generateSignableQueryStringParam($options); @@ -1782,10 +1900,10 @@ private function authPrecheckAcl($options) * 获得档次请求使用的域名 * bucket在前的三级域名,或者二级域名,如果是cname或者ip的话,则是二级域名 * - * @param $options + * @param $bucket * @return string 剥掉协议头的域名 */ - private function generateHostname($options) + private function generateHostname($bucket) { if ($this->hostType === self::OSS_HOST_TYPE_IP) { $hostname = $this->hostname; @@ -1793,7 +1911,7 @@ private function generateHostname($options) $hostname = $this->hostname; } else { // 专有域或者官网endpoint - $hostname = ($options[self::OSS_BUCKET] == '') ? $this->hostname : ($options[self::OSS_BUCKET] . '.') . $this->hostname; + $hostname = ($bucket == '') ? $this->hostname : ($bucket . '.') . $this->hostname; } return $hostname; } diff --git a/src/OSS/Result/GetCnameResult.php b/src/OSS/Result/GetCnameResult.php index 1f42e235..eed01f90 100644 --- a/src/OSS/Result/GetCnameResult.php +++ b/src/OSS/Result/GetCnameResult.php @@ -16,19 +16,4 @@ protected function parseDataFromResponse() $config->parseFromXml($content); return $config; } - - /** - * 根据返回http状态码判断,[200-299]即认为是OK, 获取bucket相关配置的接口,404也认为是一种 - * 有效响应 - * - * @return bool - */ - protected function isResponseOk() - { - $status = $this->rawResponse->status; - if ((int)(intval($status) / 100) == 2 || (int)(intval($status)) === 404) { - return true; - } - return false; - } } \ No newline at end of file diff --git a/src/OSS/Result/ListLiveChannelResult.php b/src/OSS/Result/ListLiveChannelResult.php new file mode 100644 index 00000000..18a64192 --- /dev/null +++ b/src/OSS/Result/ListLiveChannelResult.php @@ -0,0 +1,19 @@ +rawResponse->body; + $channelList = new LiveChannelListInfo(); + $channelList->parseFromXml($content); + return $channelList; + } +} \ No newline at end of file diff --git a/src/OSS/Result/PutLiveChannelResult.php b/src/OSS/Result/PutLiveChannelResult.php new file mode 100644 index 00000000..82c8662a --- /dev/null +++ b/src/OSS/Result/PutLiveChannelResult.php @@ -0,0 +1,19 @@ +rawResponse->body; + $channel = new LiveChannelInfo(); + $channel->parseFromXml($content); + return $channel; + } +} \ No newline at end of file diff --git a/tests/OSS/Tests/BucketLiveChannelTest.php b/tests/OSS/Tests/BucketLiveChannelTest.php new file mode 100644 index 00000000..a2c6d108 --- /dev/null +++ b/tests/OSS/Tests/BucketLiveChannelTest.php @@ -0,0 +1,149 @@ +client = Common::getOssClient(); + $this->bucketName = 'php-sdk-test-bucket-' . strval(rand(0, 10)); + $this->client->createBucket($this->bucketName); + } + + public function tearDown() + { + $this->client->deleteBucket($this->bucketName); + } + + public function testPutLiveChannel() + { + $config = new LiveChannelConfig(array( + 'id' => 'live-1', + 'description' => 'live channel 1', + 'type' => 'hls', + 'fragDuration' => 1000, + 'playDuration' => 5000, + 'playListName' => 'hello' + )); + $info = $this->client->putBucketLiveChannel($this->bucketName, $config); + + $this->assertEquals('live-1', $info->getId()); + $this->assertEquals('live channel 1', $info->getDescription()); + $this->assertEquals(1, count($info->getPublishUrls())); + $this->assertEquals(1, count($info->getPlayUrls())); + } + + public function testListLiveChannels() + { + $config = new LiveChannelConfig(array( + 'id' => 'live-1', + 'description' => 'live channel 1', + 'type' => 'hls', + 'fragDuration' => 1000, + 'playDuration' => 5000, + 'playListName' => 'hello' + )); + $this->client->putBucketLiveChannel($this->bucketName, $config); + + $config = new LiveChannelConfig(array( + 'id' => 'live-2', + 'description' => 'live channel 2', + 'type' => 'hls', + 'fragDuration' => 1000, + 'playDuration' => 5000, + 'playListName' => 'hello' + )); + $this->client->putBucketLiveChannel($this->bucketName, $config); + + $list = $this->client->listBucketLiveChannels($this->bucketName); + + $this->assertEquals($this->bucketName, $list->getBucketName()); + $this->assertEquals(false, $list->getIsTruncated()); + $channels = $list->getChannelList(); + $this->assertEquals(2, count($channels)); + + $chan1 = $channels[0]; + $this->assertEquals('live-1', $chan1->getId()); + $this->assertEquals('live channel 1', $chan1->getDescription()); + $this->assertEquals(1, count($chan1->getPublishUrls())); + $this->assertEquals(1, count($chan1->getPlayUrls())); + + $chan2 = $channels[1]; + $this->assertEquals('live-2', $chan2->getId()); + $this->assertEquals('live channel 2', $chan2->getDescription()); + $this->assertEquals(1, count($chan2->getPublishUrls())); + $this->assertEquals(1, count($chan2->getPlayUrls())); + + $list = $this->client->listBucketLiveChannels($this->bucketName, array( + 'prefix' => 'live-', + 'marker' => 'live-1', + 'max-keys' => 10 + )); + $channels = $list->getChannelList(); + $this->assertEquals(1, count($channels)); + $chan2 = $channels[0]; + $this->assertEquals('live-2', $chan2->getId()); + $this->assertEquals('live channel 2', $chan2->getDescription()); + $this->assertEquals(1, count($chan2->getPublishUrls())); + $this->assertEquals(1, count($chan2->getPlayUrls())); + } + + /* + public function testDeleteLiveChannel() + { + $channelId = 'live-to-delete'; + $config = new LiveChannelConfig(array( + 'id' => $channelId, + 'description' => 'live channel to delete', + 'type' => 'hls', + 'fragDuration' => 1000, + 'playDuration' => 5000, + 'playListName' => 'hello' + )); + $this->client->putBucketLiveChannel($this->bucketName, $config); + + $this->client->deleteBucketLiveChannel($channelId); + $list = $this->listLiveChannels($this->bucketName, array( + 'prefix' => $channelId + )); + + $this->assertEquals(0, count($list->getChannelList())); + } + */ + + public function testGetLiveChannelUrl() + { + $channelId = '90475'; + $bucket = 'douyu'; + $now = time(); + $url = $this->client->getLiveChannelUrl($bucket, $channelId, array( + 'expires' => 900, + 'params' => array( + 'a' => 'hello', + 'b' => 'world' + ) + )); + + $ret = parse_url($url); + $this->assertEquals('rtmp', $ret['scheme']); + parse_str($ret['query'], $query); + + $this->assertTrue(isset($query['AccessKeyId'])); + $this->assertTrue(isset($query['Signature'])); + $this->assertTrue(intval($query['Expires']) - ($now + 900) < 3); + $this->assertEquals('hello', $query['a']); + $this->assertEquals('world', $query['b']); + } +} diff --git a/tests/OSS/Tests/LiveChannelXmlTest.php b/tests/OSS/Tests/LiveChannelXmlTest.php new file mode 100644 index 00000000..45401529 --- /dev/null +++ b/tests/OSS/Tests/LiveChannelXmlTest.php @@ -0,0 +1,161 @@ + + + xxx + enabled + + hls + 1000 + 5 + hello + + +BBBB; + + private $info = << + + live-1 + xxx + + rtmp://bucket.oss-cn-hangzhou.aliyuncs.com/live/213443245345 + + + http://bucket.oss-cn-hangzhou.aliyuncs.com/213443245345/播放列表.m3u8 + + enabled + 2015-11-24T14:25:31.000Z + +BBBB; + + private $list = << + +xxx + yyy + 100 + false + 121312132 + + 12123214323431 + xxx + + rtmp://bucket.oss-cn-hangzhou.aliyuncs.com/live/1 + + + http://bucket.oss-cn-hangzhou.aliyuncs.com/1/播放列表.m3u8 + + enabled + 2015-11-24T14:25:31.000Z + + + 432423432423 + yyy + + rtmp://bucket.oss-cn-hangzhou.aliyuncs.com/live/2 + + + http://bucket.oss-cn-hangzhou.aliyuncs.com/2/播放列表.m3u8 + + enabled + 2016-11-24T14:25:31.000Z + + +BBBB; + + public function testConfig() + { + $config = new LiveChannelConfig(array('id' => 'live-1')); + $config->parseFromXml($this->config); + + $this->assertEquals('live-1', $config->getId()); + $this->assertEquals('xxx', $config->getDescription()); + $this->assertEquals('enabled', $config->getStatus()); + $this->assertEquals('hls', $config->getType()); + $this->assertEquals(1000, $config->getFragDuration()); + $this->assertEquals(5, $config->getFragCount()); + $this->assertEquals('hello', $config->getPlayListName()); + + $xml = $config->serializeToXml(); + $config2 = new LiveChannelConfig(array('id' => 'live-2')); + $config2->parseFromXml($xml); + $this->assertEquals('live-2', $config2->getId()); + $this->assertEquals('xxx', $config2->getDescription()); + $this->assertEquals('enabled', $config2->getStatus()); + $this->assertEquals('hls', $config2->getType()); + $this->assertEquals(1000, $config2->getFragDuration()); + $this->assertEquals(5, $config2->getFragCount()); + $this->assertEquals('hello', $config2->getPlayListName()); + } + + public function testInfo() + { + $info = new LiveChannelInfo(); + $info->parseFromXml($this->info); + + $this->assertEquals('live-1', $info->getId()); + $this->assertEquals('xxx', $info->getDescription()); + $this->assertEquals('enabled', $info->getStatus()); + $this->assertEquals('2015-11-24T14:25:31.000Z', $info->getLastModified()); + $pubs = $info->getPublishUrls(); + $this->assertEquals(1, count($pubs)); + $this->assertEquals('rtmp://bucket.oss-cn-hangzhou.aliyuncs.com/live/213443245345', $pubs[0]); + + $plays = $info->getPlayUrls(); + $this->assertEquals(1, count($plays)); + $this->assertEquals('http://bucket.oss-cn-hangzhou.aliyuncs.com/213443245345/播放列表.m3u8', $plays[0]); + } + + public function testList() + { + $list = new LiveChannelListInfo(); + $list->parseFromXml($this->list); + + $this->assertEquals('xxx', $list->getPrefix()); + $this->assertEquals('yyy', $list->getMarker()); + $this->assertEquals(100, $list->getMaxKeys()); + $this->assertEquals(false, $list->getIsTruncated()); + $this->assertEquals('121312132', $list->getNextMarker()); + + $channels = $list->getChannelList(); + $this->assertEquals(2, count($channels)); + + $chan1 = $channels[0]; + $this->assertEquals('12123214323431', $chan1->getId()); + $this->assertEquals('xxx', $chan1->getDescription()); + $this->assertEquals('enabled', $chan1->getStatus()); + $this->assertEquals('2015-11-24T14:25:31.000Z', $chan1->getLastModified()); + $pubs = $chan1->getPublishUrls(); + $this->assertEquals(1, count($pubs)); + $this->assertEquals('rtmp://bucket.oss-cn-hangzhou.aliyuncs.com/live/1', $pubs[0]); + + $plays = $chan1->getPlayUrls(); + $this->assertEquals(1, count($plays)); + $this->assertEquals('http://bucket.oss-cn-hangzhou.aliyuncs.com/1/播放列表.m3u8', $plays[0]); + + $chan2 = $channels[1]; + $this->assertEquals('432423432423', $chan2->getId()); + $this->assertEquals('yyy', $chan2->getDescription()); + $this->assertEquals('enabled', $chan2->getStatus()); + $this->assertEquals('2016-11-24T14:25:31.000Z', $chan2->getLastModified()); + $pubs = $chan2->getPublishUrls(); + $this->assertEquals(1, count($pubs)); + $this->assertEquals('rtmp://bucket.oss-cn-hangzhou.aliyuncs.com/live/2', $pubs[0]); + + $plays = $chan2->getPlayUrls(); + $this->assertEquals(1, count($plays)); + $this->assertEquals('http://bucket.oss-cn-hangzhou.aliyuncs.com/2/播放列表.m3u8', $plays[0]); + } +}