diff --git a/.gitignore b/.gitignore
index ced686fa..bc74c564 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,4 @@ _ReSharper*/
/src/packages
/packages
/bin
+/src/.vs/Qiniu/v16/Server/sqlite3
diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json
new file mode 100644
index 00000000..6b611411
--- /dev/null
+++ b/.vs/VSWorkspaceState.json
@@ -0,0 +1,6 @@
+{
+ "ExpandedNodes": [
+ ""
+ ],
+ "PreviewInSolutionExplorer": false
+}
\ No newline at end of file
diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite
new file mode 100644
index 00000000..d93116eb
Binary files /dev/null and b/.vs/slnx.sqlite differ
diff --git a/src/Qiniu.sln b/src/Qiniu.sln
index 7db84cf9..1c4b5ee2 100644
--- a/src/Qiniu.sln
+++ b/src/Qiniu.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.27004.2002
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28803.156
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Qiniu", "Qiniu\Qiniu.csproj", "{2F5B0328-DE8B-4B53-A500-3077E340A51B}"
EndProject
diff --git a/src/Qiniu/Pili/PiliManager.cs b/src/Qiniu/Pili/PiliManager.cs
new file mode 100644
index 00000000..9c6761f3
--- /dev/null
+++ b/src/Qiniu/Pili/PiliManager.cs
@@ -0,0 +1,79 @@
+using Qiniu.Http;
+using Qiniu.Util;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Qiniu.Pili
+{
+ ///
+ /// 直播云服务端
+ ///
+ public class PiliManager
+ {
+ private const string PILI_API_HOST = "http://pili.qiniuapi.com";
+ private Auth _auth;
+ private HttpManager _httpManager;
+ private readonly string _hub;
+ private readonly string _encodedStreamTitle;
+
+ public PiliManager(Mac mac, string hub, string streamTitle)
+ {
+ _auth = new Auth(mac);
+ _httpManager = new HttpManager();
+ _hub = hub;
+ _encodedStreamTitle = Base64.UrlSafeBase64Encode(streamTitle);
+ }
+
+ private string saveAsEntry()
+ {
+ return string.Format("{0}/v2/hubs/{1}/streams/{2}/saveas", PILI_API_HOST, _hub, _encodedStreamTitle);
+ }
+
+ ///
+ /// 录制直播回放
+ ///
+ /// 保存的文件名
+ /// 要保存的直播的起始时间
+ /// 要保存的直播的结束时间
+ /// 保存的文件格式
+ /// 数据处理的私有队列
+ /// 保存成功回调通知地址
+ /// 更改ts文件的过期时间
+ ///
+ public SaveAsResult SaveAs(string fname = "", long start = 0, long end = 0, string format = "", string pipeline = "", string notify = "", int expireDays = 0)
+ {
+ SaveAsRequest request = new SaveAsRequest(fname, start, end, format, pipeline, notify, expireDays);
+
+ SaveAsResult result = new SaveAsResult();
+
+ try
+ {
+ string url = saveAsEntry();
+ string body = request.ToJsonStr();
+ string token = _auth.CreateStreamManageToken(url, body);
+
+ HttpResult hr = _httpManager.PostJson(url, body, token);
+ result.Shadow(hr);
+ }
+ catch (Exception ex)
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.AppendFormat("[{0}] [saveas] Error: ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"));
+ Exception e = ex;
+ while (e != null)
+ {
+ sb.Append(e.Message + " ");
+ e = e.InnerException;
+ }
+ sb.AppendLine();
+
+ result.RefCode = (int)HttpCode.INVALID_ARGUMENT;
+ result.RefText += sb.ToString();
+ }
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Qiniu/Pili/SaveAsInfo.cs b/src/Qiniu/Pili/SaveAsInfo.cs
new file mode 100644
index 00000000..a5dd4bd4
--- /dev/null
+++ b/src/Qiniu/Pili/SaveAsInfo.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Qiniu.Pili
+{
+ ///
+ /// 录制直播回放-消息内容结果
+ ///
+ public class SaveAsInfo
+ {
+ ///
+ /// 代码 含义 说明
+ /// 200 success 成功(OK)
+ /// 612 stream not found
+ /// 619 no data 没有直播数据
+ ///
+ public int Code { get; set; }
+
+ ///
+ /// 错误消息(状态码非OK时)
+ ///
+ public string Error { get; set; }
+
+ ///
+ /// 保存后在存储空间里的文件名
+ ///
+ public string FName { get; set; }
+
+ ///
+ /// 持久化异步处理任务ID,异步模式才会返回该字段,可以通过该字段查询转码进度
+ ///
+ public string PersistentID { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Qiniu/Pili/SaveAsRequest.cs b/src/Qiniu/Pili/SaveAsRequest.cs
new file mode 100644
index 00000000..bd116f77
--- /dev/null
+++ b/src/Qiniu/Pili/SaveAsRequest.cs
@@ -0,0 +1,94 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Qiniu.Pili
+{
+ ///
+ /// 录制直播回放-请求
+ ///
+ public class SaveAsRequest
+ {
+ ///
+ /// 保存的文件名,不指定系统会随机生成
+ ///
+ [JsonProperty("fname")]
+ public string FileName { get; set; }
+ ///
+ /// 整数,Unix 时间戳,要保存的直播的起始时间,不指定或 0 值表示从第一次直播开始
+ ///
+ [JsonProperty("start")]
+ public long StartTimestamp { get; set; }
+ ///
+ /// 整数,Unix 时间戳,要保存的直播的结束时间,不指定或 0 值表示当前时间
+ ///
+ [JsonProperty("end")]
+ public long EndTimestamp { get; set; }
+ ///
+ /// 保存的文件格式,默认为m3u8,如果指定其他格式,则保存动作为异步模式。详细信息可以参考 转码 的api
+ ///
+ [JsonProperty("format")]
+ public string Format { get; set; }
+ ///
+ /// 异步模式时,数据处理的私有队列,不指定则使用公共队列
+ ///
+ [JsonProperty("pipeline")]
+ public string Pipeline { get; set; }
+ ///
+ /// 异步模式时,保存成功回调通知地址,不指定则不通知
+ ///
+ [JsonProperty("notify")]
+ public string Notify { get; set; }
+ ///
+ /// 更改ts文件的过期时间,默认为永久保存。-1 表示不更改ts文件的生命周期,正值表示修改ts文件的生命周期为expireDays
+ ///
+ [JsonProperty("expireDays")]
+ public int ExpireDays { get; set; }
+
+ ///
+ /// 初始化(所有成员为空,需要后续赋值)
+ ///
+ public SaveAsRequest()
+ {
+ FileName = "";
+ StartTimestamp = 0;
+ EndTimestamp = 0;
+ Format = "";
+ Pipeline = "";
+ Notify = "";
+ ExpireDays = 0;
+ }
+
+ ///
+ /// 初始化所有成员
+ ///
+ /// 保存的文件名
+ /// 要保存的直播的起始时间
+ /// 要保存的直播的结束时间
+ /// 保存的文件格式
+ /// 数据处理的私有队列
+ /// 保存成功回调通知地址
+ /// 更改ts文件的过期时间
+ public SaveAsRequest(string fname, long start, long end, string format, string pipeline, string notify, int expireDays)
+ {
+ FileName = fname;
+ StartTimestamp = start;
+ EndTimestamp = end;
+ Format = format;
+ Pipeline = pipeline;
+ Notify = notify;
+ ExpireDays = expireDays;
+ }
+
+ ///
+ /// 转换到JSON字符串
+ ///
+ /// 请求内容的JSON字符串
+ public string ToJsonStr()
+ {
+ return JsonConvert.SerializeObject(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Qiniu/Pili/SaveAsResult.cs b/src/Qiniu/Pili/SaveAsResult.cs
new file mode 100644
index 00000000..a5468f2c
--- /dev/null
+++ b/src/Qiniu/Pili/SaveAsResult.cs
@@ -0,0 +1,90 @@
+using Newtonsoft.Json;
+using Qiniu.Http;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Qiniu.Pili
+{
+ ///
+ /// 录制直播回放-结果
+ ///
+ public class SaveAsResult : HttpResult
+ {
+ ///
+ /// 获取带宽信息
+ ///
+ public SaveAsInfo Result
+ {
+ get
+ {
+ SaveAsInfo info = null;
+ if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text)))
+ {
+ info = JsonConvert.DeserializeObject(Text);
+ }
+ return info;
+ }
+ }
+
+ ///
+ /// 转换为易读字符串格式
+ ///
+ /// 便于打印和阅读的字符串
+ public override string ToString()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.AppendFormat("code:{0}\n", Code);
+
+ sb.AppendLine();
+
+ if (Result != null)
+ {
+ sb.AppendLine("result:");
+ sb.AppendFormat("code:{0}\n", Result.Code);
+ if (!string.IsNullOrEmpty(Result.Error))
+ {
+ sb.AppendFormat("error:{0}\n", Result.Error);
+ }
+ if (!string.IsNullOrEmpty(Result.FName))
+ {
+ sb.AppendFormat("fname:{0}\n", Result.FName);
+ }
+ if (!string.IsNullOrEmpty(Result.PersistentID))
+ {
+ sb.AppendFormat("persistentID:{0}\n", Result.PersistentID);
+ }
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(Text))
+ {
+ sb.AppendLine("text:");
+ sb.AppendLine(Text);
+ }
+ }
+ sb.AppendLine();
+
+ sb.AppendFormat("ref-code:{0}\n", RefCode);
+
+ if (!string.IsNullOrEmpty(RefText))
+ {
+ sb.AppendLine("ref-text:");
+ sb.AppendLine(RefText);
+ }
+
+ if (RefInfo != null)
+ {
+ sb.AppendFormat("ref-info:\n");
+ foreach (var d in RefInfo)
+ {
+ sb.AppendLine(string.Format("{0}:{1}", d.Key, d.Value));
+ }
+ }
+
+ return sb.ToString();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Qiniu/Qiniu.csproj b/src/Qiniu/Qiniu.csproj
index 588c7313..9bd98845 100644
--- a/src/Qiniu/Qiniu.csproj
+++ b/src/Qiniu/Qiniu.csproj
@@ -79,8 +79,14 @@
+
+
+
+
+
+
@@ -156,6 +162,7 @@
+
diff --git a/src/Qiniu/RTC/RTCManager.cs b/src/Qiniu/RTC/RTCManager.cs
new file mode 100644
index 00000000..8e33424e
--- /dev/null
+++ b/src/Qiniu/RTC/RTCManager.cs
@@ -0,0 +1,37 @@
+using Qiniu.Util;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Qiniu.RTC
+{
+ ///
+ /// 实时音视频服务端
+ ///
+ public class RTCManager
+ {
+ private const string RTC_API_HOST = "http://rtc.qiniuapi.com";
+ private Auth _auth;
+
+ public RTCManager(Mac mac)
+ {
+ _auth = new Auth(mac);
+ }
+
+ ///
+ /// RoomToken签发服务
+ ///
+ /// 房间所属账号的AppID
+ /// 房间名称
+ /// 请求加入房间的用户ID
+ /// 鉴权的有效时间
+ /// 该用户的房间管理权限
+ /// RoomToken
+ public string GetRoomToken(string appId, string roomName, string userId, long expireAt, string permission)
+ {
+ RoomTokenRequest request = new RoomTokenRequest(appId, roomName, userId, expireAt, permission);
+ return _auth.CreateUploadToken(request.ToJsonStr());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Qiniu/RTC/RoomTokenRequest.cs b/src/Qiniu/RTC/RoomTokenRequest.cs
new file mode 100644
index 00000000..0bfcfce8
--- /dev/null
+++ b/src/Qiniu/RTC/RoomTokenRequest.cs
@@ -0,0 +1,78 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace Qiniu.RTC
+{
+ ///
+ /// 房间管理凭证-请求
+ ///
+ public class RoomTokenRequest
+ {
+ ///
+ /// 房间所属账号的AppID
+ ///
+ [JsonProperty("appId")]
+ public string AppID { get; set; }
+ ///
+ /// 房间名称
+ ///
+ [JsonProperty("roomName")]
+ public string RoomName { get; set; }
+ ///
+ /// 请求加入房间的用户ID
+ ///
+ [JsonProperty("userId")]
+ public string UserID { get; set; }
+ ///
+ /// 鉴权的有效时间,传入以秒为单位的 64 位 Unix 绝对时间,token 将在该时间后失效
+ ///
+ [JsonProperty("expireAt")]
+ public long ExpireAt { get; set; }
+ ///
+ /// 该用户的房间管理权限,"admin" 或 "user"
+ ///
+ [JsonProperty("permission")]
+ public string Permission { get; set; }
+
+ ///
+ /// 初始化(所有成员为空,需要后续赋值)
+ ///
+ public RoomTokenRequest()
+ {
+ AppID = "";
+ RoomName = "";
+ UserID = "";
+ ExpireAt = 0;
+ Permission = "";
+ }
+
+ ///
+ /// 初始化所有成员
+ ///
+ /// 房间所属账号的AppID
+ /// 房间名称
+ /// 请求加入房间的用户ID
+ /// 鉴权的有效时间
+ /// 该用户的房间管理权限
+ public RoomTokenRequest(string appId, string roomName, string userId, long expireAt, string permission)
+ {
+ AppID = appId;
+ RoomName = roomName;
+ UserID = userId;
+ ExpireAt = expireAt;
+ Permission = permission;
+ }
+
+ ///
+ /// 转换到JSON字符串
+ ///
+ /// 请求内容的JSON字符串
+ public string ToJsonStr()
+ {
+ return JsonConvert.SerializeObject(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Qiniu/Util/Auth.cs b/src/Qiniu/Util/Auth.cs
index 0fe4934f..4021ab4d 100644
--- a/src/Qiniu/Util/Auth.cs
+++ b/src/Qiniu/Util/Auth.cs
@@ -24,7 +24,7 @@ public Auth(Mac mac)
/// 请求的URL
/// 请求的主体内容
/// 生成的管理凭证
- public string CreateManageToken(string url,byte[] body)
+ public string CreateManageToken(string url, byte[] body)
{
return string.Format("QBox {0}", signature.SignRequest(url, body));
}
@@ -72,11 +72,12 @@ public string CreateStreamPublishToken(string path)
///
/// 生成流管理凭证
///
- ///
- ///
- public string CreateStreamManageToken(string data)
+ /// 访问的URL
+ /// 请求的数据
+ /// 生成的流管理凭证
+ public string CreateStreamManageToken(string url, string data)
{
- return string.Format("Qiniu {0}", signature.SignWithData(data));
+ return string.Format("Qiniu {0}", signature.SignStreamManageRequest(url, data));
}
#region STATIC
@@ -137,7 +138,7 @@ public static string CreateDownloadToken(Mac mac, string url)
/// 账号(密钥)
/// URL路径
///
- public static string CreateStreamPublishToken(Mac mac,string path)
+ public static string CreateStreamPublishToken(Mac mac, string path)
{
Signature sx = new Signature(mac);
return sx.Sign(path);
diff --git a/src/Qiniu/Util/Signature.cs b/src/Qiniu/Util/Signature.cs
index 37919a60..edb3c870 100644
--- a/src/Qiniu/Util/Signature.cs
+++ b/src/Qiniu/Util/Signature.cs
@@ -1,4 +1,5 @@
-using System;
+using Qiniu.Http;
+using System;
using System.IO;
#if WINDOWS_UWP
using Windows.Security.Cryptography;
@@ -111,6 +112,34 @@ public string SignRequest(string url, byte[] body)
}
}
+ ///
+ /// 直播流管理请求签名
+ ///
+ /// 请求目标的URL
+ /// 请求的主体数据
+ /// 直播流管理请求签名
+ public string SignStreamManageRequest(string url, string body)
+ {
+ string data = "POST ";
+
+ Uri u = new Uri(url);
+ string pathAndQuery = u.PathAndQuery;
+
+ data += pathAndQuery;
+ data += string.Format("\nHost: {0}", u.Host);
+ data += string.Format("\nContent-Type: {0}",ContentType.APPLICATION_JSON);
+ data += "\n\n";
+ if (!string.IsNullOrWhiteSpace(body))
+ {
+ data += body;
+ }
+
+ HMACSHA1 hmac = new HMACSHA1(Encoding.UTF8.GetBytes(mac.SecretKey));
+ byte[] digest = hmac.ComputeHash(Encoding.UTF8.GetBytes(data));
+ string digestBase64 = Base64.UrlSafeBase64Encode(digest);
+ return string.Format("{0}:{1}", mac.AccessKey, digestBase64);
+ }
+
///
/// HTTP请求签名
///