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 +{ + /// <summary> + /// 直播云服务端 + /// </summary> + 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); + } + + /// <summary> + /// 录制直播回放 + /// </summary> + /// <param name="fname">保存的文件名</param> + /// <param name="start">要保存的直播的起始时间</param> + /// <param name="end">要保存的直播的结束时间</param> + /// <param name="format">保存的文件格式</param> + /// <param name="pipeline">数据处理的私有队列</param> + /// <param name="notify">保存成功回调通知地址</param> + /// <param name="expireDays">更改ts文件的过期时间</param> + /// <returns></returns> + 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 +{ + /// <summary> + /// 录制直播回放-消息内容结果 + /// </summary> + public class SaveAsInfo + { + /// <summary> + /// 代码 含义 说明 + /// 200 success 成功(OK) + /// 612 stream not found + /// 619 no data 没有直播数据 + /// </summary> + public int Code { get; set; } + + /// <summary> + /// 错误消息(状态码非OK时) + /// </summary> + public string Error { get; set; } + + /// <summary> + /// 保存后在存储空间里的文件名 + /// </summary> + public string FName { get; set; } + + /// <summary> + /// 持久化异步处理任务ID,异步模式才会返回该字段,可以通过该字段查询转码进度 + /// </summary> + 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 +{ + /// <summary> + /// 录制直播回放-请求 + /// </summary> + public class SaveAsRequest + { + /// <summary> + /// 保存的文件名,不指定系统会随机生成 + /// </summary> + [JsonProperty("fname")] + public string FileName { get; set; } + /// <summary> + /// 整数,Unix 时间戳,要保存的直播的起始时间,不指定或 0 值表示从第一次直播开始 + /// </summary> + [JsonProperty("start")] + public long StartTimestamp { get; set; } + /// <summary> + /// 整数,Unix 时间戳,要保存的直播的结束时间,不指定或 0 值表示当前时间 + /// </summary> + [JsonProperty("end")] + public long EndTimestamp { get; set; } + /// <summary> + /// 保存的文件格式,默认为m3u8,如果指定其他格式,则保存动作为异步模式。详细信息可以参考 转码 的api + /// </summary> + [JsonProperty("format")] + public string Format { get; set; } + /// <summary> + /// 异步模式时,数据处理的私有队列,不指定则使用公共队列 + /// </summary> + [JsonProperty("pipeline")] + public string Pipeline { get; set; } + /// <summary> + /// 异步模式时,保存成功回调通知地址,不指定则不通知 + /// </summary> + [JsonProperty("notify")] + public string Notify { get; set; } + /// <summary> + /// 更改ts文件的过期时间,默认为永久保存。-1 表示不更改ts文件的生命周期,正值表示修改ts文件的生命周期为expireDays + /// </summary> + [JsonProperty("expireDays")] + public int ExpireDays { get; set; } + + /// <summary> + /// 初始化(所有成员为空,需要后续赋值) + /// </summary> + public SaveAsRequest() + { + FileName = ""; + StartTimestamp = 0; + EndTimestamp = 0; + Format = ""; + Pipeline = ""; + Notify = ""; + ExpireDays = 0; + } + + /// <summary> + /// 初始化所有成员 + /// </summary> + /// <param name="fname">保存的文件名</param> + /// <param name="start">要保存的直播的起始时间</param> + /// <param name="end">要保存的直播的结束时间</param> + /// <param name="format">保存的文件格式</param> + /// <param name="pipeline">数据处理的私有队列</param> + /// <param name="notify">保存成功回调通知地址</param> + /// <param name="expireDays">更改ts文件的过期时间</param> + 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; + } + + /// <summary> + /// 转换到JSON字符串 + /// </summary> + /// <returns>请求内容的JSON字符串</returns> + 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 +{ + /// <summary> + /// 录制直播回放-结果 + /// </summary> + public class SaveAsResult : HttpResult + { + /// <summary> + /// 获取带宽信息 + /// </summary> + public SaveAsInfo Result + { + get + { + SaveAsInfo info = null; + if ((Code == (int)HttpCode.OK) && (!string.IsNullOrEmpty(Text))) + { + info = JsonConvert.DeserializeObject<SaveAsInfo>(Text); + } + return info; + } + } + + /// <summary> + /// 转换为易读字符串格式 + /// </summary> + /// <returns>便于打印和阅读的字符串</returns> + 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 @@ <Reference Include="System.Xml" /> </ItemGroup> <ItemGroup> + <Compile Include="Pili\PiliManager.cs" /> + <Compile Include="Pili\SaveAsInfo.cs" /> + <Compile Include="Pili\SaveAsRequest.cs" /> + <Compile Include="Pili\SaveAsResult.cs" /> <Compile Include="QiniuCSharpSDK.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> + <Compile Include="RTC\RoomTokenRequest.cs" /> + <Compile Include="RTC\RTCManager.cs" /> <Compile Include="Storage\Config.cs" /> <Compile Include="Storage\FetchInfo.cs" /> <Compile Include="Storage\FetchResult.cs" /> @@ -156,6 +162,7 @@ <None Include="packages.config" /> <None Include="QiniuCSharpSDK.snk" /> </ItemGroup> + <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <PropertyGroup> <PostBuildEvent> 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 +{ + /// <summary> + /// 实时音视频服务端 + /// </summary> + public class RTCManager + { + private const string RTC_API_HOST = "http://rtc.qiniuapi.com"; + private Auth _auth; + + public RTCManager(Mac mac) + { + _auth = new Auth(mac); + } + + /// <summary> + /// RoomToken签发服务 + /// </summary> + /// <param name="appId">房间所属账号的AppID</param> + /// <param name="roomName">房间名称</param> + /// <param name="userId">请求加入房间的用户ID</param> + /// <param name="expireAt">鉴权的有效时间</param> + /// <param name="permission">该用户的房间管理权限</param> + /// <returns>RoomToken</returns> + 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 +{ + /// <summary> + /// 房间管理凭证-请求 + /// </summary> + public class RoomTokenRequest + { + /// <summary> + /// 房间所属账号的AppID + /// </summary> + [JsonProperty("appId")] + public string AppID { get; set; } + /// <summary> + /// 房间名称 + /// </summary> + [JsonProperty("roomName")] + public string RoomName { get; set; } + /// <summary> + /// 请求加入房间的用户ID + /// </summary> + [JsonProperty("userId")] + public string UserID { get; set; } + /// <summary> + /// 鉴权的有效时间,传入以秒为单位的 64 位 Unix 绝对时间,token 将在该时间后失效 + /// </summary> + [JsonProperty("expireAt")] + public long ExpireAt { get; set; } + /// <summary> + /// 该用户的房间管理权限,"admin" 或 "user" + /// </summary> + [JsonProperty("permission")] + public string Permission { get; set; } + + /// <summary> + /// 初始化(所有成员为空,需要后续赋值) + /// </summary> + public RoomTokenRequest() + { + AppID = ""; + RoomName = ""; + UserID = ""; + ExpireAt = 0; + Permission = ""; + } + + /// <summary> + /// 初始化所有成员 + /// </summary> + /// <param name="appId">房间所属账号的AppID</param> + /// <param name="roomName">房间名称</param> + /// <param name="userId">请求加入房间的用户ID</param> + /// <param name="expireAt">鉴权的有效时间</param> + /// <param name="permission">该用户的房间管理权限</param> + public RoomTokenRequest(string appId, string roomName, string userId, long expireAt, string permission) + { + AppID = appId; + RoomName = roomName; + UserID = userId; + ExpireAt = expireAt; + Permission = permission; + } + + /// <summary> + /// 转换到JSON字符串 + /// </summary> + /// <returns>请求内容的JSON字符串</returns> + 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) /// <param name="url">请求的URL</param> /// <param name="body">请求的主体内容</param> /// <returns>生成的管理凭证</returns> - 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) /// <summary> /// 生成流管理凭证 /// </summary> - /// <param name="data"></param> - /// <returns></returns> - public string CreateStreamManageToken(string data) + /// <param name="url">访问的URL</param> + /// <param name="data">请求的数据</param> + /// <returns>生成的流管理凭证</returns> + 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) /// <param name="mac">账号(密钥)</param> /// <param name="path">URL路径</param> /// <returns></returns> - 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) } } + /// <summary> + /// 直播流管理请求签名 + /// </summary> + /// <param name="url">请求目标的URL</param> + /// <param name="body">请求的主体数据</param> + /// <returns>直播流管理请求签名</returns> + 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); + } + /// <summary> /// HTTP请求签名 /// </summary>