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请求签名 ///