--- tags: C# --- # C# - Video File Streaming **Demo 專案連結** : [VideoStreamingAPI](http://192.168.1.136/JeremyWu/VideoStreamingAPI) 實作可支援 Partial Content 及 Download/Play Speed Limit 的串流影片 API ## 1. Download/Play Speed Limit ### Create a Web API Project 參考文件 - [Tutorial: Create a web API with ASP.NET Core MVC](https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/first-web-api?view=aspnetcore-2.2&tabs=visual-studio) ### Use WebConfig to set default download speed limit and initial directory 於 Web.config 中新增 ```csharp= <appSettings> <!-- default: 1024000bytes ≒ 1024KB ≒ 1MB --> <add key="DefaultBps" value="1024000" /> <add key="InitialDirectory" value="C:/Users/Jeremy Wu/Videos" /> </appSettings> ``` ### VideoStream Constructor 1. 讀取檔案使用 [FileInfo](https://docs.microsoft.com/zh-tw/dotnet/api/system.io.fileinfo?view=netframework-4.7.2) 2. 取得影片 metadata 使用 [MediaToolKit](https://github.com/AydinAdn/MediaToolkit) 的 MediaFile 和 GetMetadata method 3. 取得 WebConfig 中的設定使用 [ZapLib Config API](https://linzap.gitbooks.io/zaplib/content/config-api/config-api.html) 的 get method 4. 取得影片是否含有 B frame 使用 [NReco.VideoInfo](https://www.nrecosite.com/video_info_net.aspx) 的 FFProbe 和 GetMediaInfo method ```csharp= // videostream constructor var video = new VideoStream(); video.fileInfo = new FileInfo(Path.Combine(Config.get("InitialDirectory"), filename)); video.mediaFile = new MediaFile(Path.Combine(Config.get("InitialDirectory"), filename)); video.GetMetadata(); // find video have B frame or not var videoInfo = new FFProbe(); var has_b_frames = videoInfo.GetMediaInfo(Path.Combine(Config.get("InitialDirectory"), filename)).Result.CreateNavigator().OuterXml.Contains("has_b_frames=\"1\""); // set bps video.maximumBytesPerSecond = speed == null || speed < 0 ? has_b_frames ? Convert.ToInt64(Config.get("DefaultBps")) : (long)(video.mediaFile.Metadata.VideoData.BitRateKbs / 8 + 100) * 1000 /* 最低傳輸速度 */ : Convert.ToInt64(speed) * 1000 ; ``` ### Set Response Content 提供以 Stream 為基礎的 HTTP 內容則使用 [PushStreamContent](https://docs.microsoft.com/en-us/previous-versions/aspnet/hh995285(v%3Dvs.118)) ```csharp= response.Content = new PushStreamContent((Action<Stream, HttpContent, TransportContext>)video.WriteToStream, new MediaTypeHeaderValue("video/mp4")); ``` WriteToStream 為實作讀進 buffer ([Read](https://docs.microsoft.com/zh-tw/dotnet/api/system.io.stream.read?view=netframework-4.7.2)) 和寫入 stream ([WriteAsync](https://docs.microsoft.com/zh-tw/dotnet/api/system.io.stream.writeasync?view=netframework-4.7.2)) 的 method [Throttle](#Implement-Download-Speed-Limit) 為實作 Speed Limit 的 method,之後會介紹 ```csharp= public async void WriteToStream(Stream outputStream, HttpContent content, TransportContext context) { try { var buffer = new byte[maximumBytesPerSecond <= 102400000 /* 100 MB/s */? maximumBytesPerSecond : 102400000]; using (var video = fileInfo.OpenRead()) { var length = (int)video.Length; var bytesRead = 1; while (length > 0 && bytesRead > 0) { // limit download sPeed Throttle(Math.Min(length, buffer.Length)); bytesRead = video.Read(buffer, 0, Math.Min(length, buffer.Length)); await outputStream.WriteAsync(buffer, 0, bytesRead); length -= bytesRead; } } } catch (HttpException) return; finally outputStream.Close(); } ``` ### Implement Download Speed Limit 參考文件 - [Bandwidth throttling](https://www.codeproject.com/Articles/18243/Bandwidth-throttling) 限制下載速度為使用其他人撰寫的 Class,雖然沒有非常了解怎麼實作的,但大致能說明其概念 概念為每次進行 [Read](https://docs.microsoft.com/zh-tw/dotnet/api/system.io.stream.read?view=netframework-4.7.2) 和 [WriteAsync](https://docs.microsoft.com/zh-tw/dotnet/api/system.io.stream.writeasync?view=netframework-4.7.2) 前執行 Throttle 計算目前 bps 是否已超過設定之上限 若超過則 Sleep,達到限制下載速度功能 #### 程式碼 : ```csharp= /// <summary> /// Throttles for the specified buffer size in bytes. /// </summary> /// <param name="bufferSizeInBytes">The buffer size in bytes.</param> protected void Throttle(int bufferSizeInBytes) { // Make sure the buffer isn't empty. if (_maximumBytesPerSecond <= 0 || bufferSizeInBytes <= 0) { return; } _byteCount += bufferSizeInBytes; long elapsedMilliseconds = CurrentMilliseconds - _start; if (elapsedMilliseconds > 0) { // Calculate the current bps. long bps = _byteCount * 1000L / elapsedMilliseconds; // If the bps are more then the maximum bps, try to throttle. if (bps > _maximumBytesPerSecond) { // Calculate the time to sleep. long wakeElapsed = _byteCount * 1000L / _maximumBytesPerSecond; int toSleep = (int)(wakeElapsed - elapsedMilliseconds); if (toSleep > 1) { try { // The time to sleep is more then a millisecond, so sleep. Thread.Sleep(toSleep); } catch (ThreadAbortException) { // Eatup ThreadAbortException. } // A sleep has been done, reset the bytecount and starttime. Reset(); } } } } ``` #### Flow chart 圖解 : ![](https://i.imgur.com/UU227xt.png) ### Set response header API 提供播放及下載檔案時須設定 Header,其設定如下 * [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) (.NET: [ContentType](https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.headers.httpcontentheaders.contenttype?view=netframework-4.7.2#System_Net_Http_Headers_HttpContentHeaders_ContentType)) Indicates the **media type** of the resource ```csharp= response.Content.Headers.ContentType = new MediaTypeHeaderValue("video/mp4"); ``` * [Content-Disposition](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) (.NET: [ContentDisposition](https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.headers.httpcontentheaders.contentdisposition?view=netframework-4.7.2#System_Net_Http_Headers_HttpContentHeaders_ContentDisposition)) Indicating the content is expected to be **displayed inline in the browser**, **or as an attachment**, that is downloaded and saved locally ```csharp= response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); response.Content.Headers.ContentDisposition.FileName = fileName; ``` ## 2. Partial Content ### Get request header Header 中的 [Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) 會明確表示欲存取的片段,可藉由 [HttpRequestHeaders.Range](https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.headers.httprequestheaders.range?view=netframework-4.7.2) 取得,<br/>其 property value 為 [RangeHeaderValue](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.headers.rangeheadervalue?view=netframework-4.7.2) ```csharp= RangeHeaderValue rangeHeader = Request.Headers.Range; ``` 而 [RangeHeaderValue.Ranges](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.headers.rangeheadervalue.ranges?view=netframework-4.7.2#System_Net_Http_Headers_RangeHeaderValue_Ranges) 為 [ICollection](https://docs.microsoft.com/zh-tw/dotnet/api/system.collections.generic.icollection-1?view=netframework-4.7.2)<[RangeItemHeaderValue](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.headers.rangeitemheadervalue?view=netframework-4.7.2)>, 需轉換成 [List](https://docs.microsoft.com/zh-tw/dotnet/api/system.collections.generic.list-1?view=netframework-4.7.2) 後才能取得影片開始存取 ([From](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.headers.rangeitemheadervalue.from?view=netframework-4.7.2#System_Net_Http_Headers_RangeItemHeaderValue_From)) 和結束存取 ([To](https://docs.microsoft.com/en-us/dotnet/api/system.net.http.headers.rangeitemheadervalue.to?view=netframework-4.7.2#System_Net_Http_Headers_RangeItemHeaderValue_To)) 位置 ```csharp= var range = new List<RangeItemHeaderValue>(rangeHeader.Ranges)[0]; video.videoStart = range.From ?? 0; video.videoEnd = range.To ?? totalLength - 1; ``` ### Implement Partial Content 參考文件 - [HTTP 206 Partial Content In ASP.NET Web API - Video File Streaming](https://www.codeproject.com/Articles/820146/HTTP-Partial-Content-In-ASP-NET-Web-API-Video) [STREAMING VIDEO CONTENT TO A BROWSER USING WEB API](https://blogs.ibs.com/2017/01/24/streaming-video-content-to-a-browser-using-web-api/) 透過 [FileStream.Position](https://docs.microsoft.com/zh-tw/dotnet/api/system.io.filestream.position?view=netframework-4.7.2#System_IO_FileStream_Position) ,設定要從何處 (videoStart) 開始讀取 Video Stream 接著一直讀取直到 position 已達結束存取 (videoEnd) 位置 ```csharp= public async void PartialWriteToStream(Stream outputStream, HttpContent content, TransportContext context) { try { var buffer = new byte[maximumBytesPerSecond <= 102400000 /* 100 MB/s */? maximumBytesPerSecond : 102400000]; using (var video = fileInfo.OpenRead()) { var position = videoStart; var bytesLeft = videoEnd - videoStart + 1; // set position video.Position = videoStart; var bytesRead = 1; while (position <= videoEnd) { Throttle((int)Math.Min(bytesLeft, buffer.Length)); bytesRead = video.Read(buffer, 0, (int)Math.Min(bytesLeft, buffer.Length)); await outputStream.WriteAsync(buffer, 0, bytesRead); position += bytesRead; bytesLeft = videoEnd - position + 1; } } } catch (HttpException) return; finally outputStream.Close(); } ``` ### Set response header API 支援 Partial Content 時須另外設定 Header,其設定如下 * [Accept-Ranges](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges) (.NET: [AcceptRanges](https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.headers.httpresponseheaders.acceptranges?view=netframework-4.7.2)) Used by the server to advertise its support of partial requests ```csharp= response.Headers.AcceptRanges.Add("bytes"); ``` * [Content-Length](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length) (.NET: [ContentLength](https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.headers.httpcontentheaders.contentlength?view=netframework-4.7.2)) Indicates the size of the entity-body, in bytes, sent to the recipient ```csharp= response.Content.Headers.ContentLength = video.videoEnd - video.videoStart + 1; ``` * [Content-Range](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range) (.NET: [ContentRange](https://docs.microsoft.com/zh-tw/dotnet/api/system.net.http.headers.httpcontentheaders.contentrange?view=netframework-4.7.2)) Indicates where in a full body message a partial message belongs ```csharp= response.Content.Headers.ContentRange = new ContentRangeHeaderValue(video.videoStart, video.videoEnd, totalLength); ``` ## 3. Test Video API 請自行新增影片於任一資料夾中,並於 Web.Config [設定 initial directory](#Use-WebConfig-to-set-deafult-download-speed-limit-and-initial-directory) **Path** ``` GET /api/video ``` **Query Params** | Field | type | required | description | | -------- | ------ | -------- | -------------------------------- | | filename | string | Y | 檔名 | | speed | int | N | 下載速度 (KB/s), default 為 1024KB | 1. 測試限制下載速度 於網址列輸入 http://192.168.1.61:923/api/video?filename=type2.mp4&speed=1000 2. 測試 Partial Content 隨便寫個 html 網頁,測試是否成功實作 (src 需自行更改) ```htmlembedded= <video width="800" controls src="http://192.168.1.61:923/api/video?filename=type2_big.mp4&speed=5000" > </video> ``` ## 4. Research ### 影片種類 影片素材連結: https://iqservice-my.sharepoint.com/:f:/g/personal/jeremywu_iqs-t_com/EgH074791wBEqvGRxtm6OtIBitQEpbKNg8x1zMI6Iduiyw?e=tL2Zqj 播放的影片可分為以下三種: 1. type1.mp4 - 將metadata,像是片長、位元速率等屬性,放置於檔案的開頭 2. type2.mp4 - 將metadata,像是片長、位元速率等屬性,放置於檔案的最後 3. type3.mp4 - 影片含有 B frame,其本身不得為其他 frame 所參考,所以無法跳著播放影片 ( 參考文件 - [影片無法跳著看這是為什麼呢???](https://www.coolaler.com/threads/83723/#post-1037536) ) 所以播放第三種影片時,需將影片全部 load 完,才可播放 ![](https://i.imgur.com/r6ZeSPG.png) ### 影片 metadata #### 1. type1.mp4 metadata: ![](https://i.imgur.com/Yzu7Hzp.png) ![](https://i.imgur.com/UBYnJhc.png) #### 2. type2.mp4 metadata: ![](https://i.imgur.com/Yzu7Hzp.png) ![](https://i.imgur.com/8FFzlci.png) #### 3. type3.mp4 metadata: ![](https://i.imgur.com/zQUYeSd.png) ![](https://i.imgur.com/0ibbbGX.png) ### 傳輸流量測試 以下為在「不跳著播放影片」的條件下測試播放是否順暢 #### 1. 對於解析度及流量做測試 | 影片名 | 檔案大小 | 解析度 | 位元速率(bitrate) | 需多少流量能順暢播放 | | --------- | ------- | ----------- | ----------------- | ---------------- | | type1.mp4 | 7.65MB | 960 * 540 | 937kbps(bit/s) | 200KB/s(Byte/s) | | test.mp4 | 43.0MB | 1920 * 1080 | 1052kbps | 200KB/s | | type2.mp4 | 65.5MB | 1920 * 1080 | 18120kbps | 2.5MB/s | #### 2. 對於同一解析度、流量,但格式不同做測試 | 影片名 | 檔案大小 | 解析度 | 位元速率(bitrate) | 需多少流量能順暢播放 | | --------- | ------- | ----------- | ----------------- | ---------------- | | test.mp4 | 43.0MB | 1920 * 1080 | 1052kbps | 200KB/s | | test.m4v | 43.0MB | 1920 * 1080 | 1052kbps | 200KB/s | | test.mov | 43.0MB | 1920 * 1080 | 1052kbps | 200KB/s | **結論:** 1. 檔案大小、解析度及格式,與傳輸流量並無太大關係 2. 傳輸流量會與 bit rate 有絕對關係,**bit rate 越高,傳輸流量需越高** 理論上若**傳輸流量(KB) 大於影片的 bitrate(kbps) / 8**,則可順暢播放 而實際傳輸流量須更大一些,才不會轉圈圈,並能讓影片緩衝時間變短,進而較快播放 ( 參考文件 - [技術專欄:線上影音播放的最大挑戰及因應(一)](http://www.onevision.com.tw/blog/index.php/01/12/%E6%8A%80%E8%A1%93%E5%B0%88%E6%AC%84%EF%BC%9A%E7%B7%9A%E4%B8%8A%E5%BD%B1%E9%9F%B3%E6%92%AD%E6%94%BE%E7%9A%84%E6%9C%80%E5%A4%A7%E6%8C%91%E6%88%B0%E5%8F%8A%E5%9B%A0%E6%87%89%E4%B8%80/) ) ## 5. Problem ### Can't Find csc.exe Build 專案後可能會遇到以下問題 ![](https://i.imgur.com/1qgHjAB.png) 請參考下列文章之解決辦法 1. [[.NET] Web API找不到 bin 底下的 roslyn csc.exe ?](http://marcus116.blogspot.com/2018/11/net-web-api-bin-roslyn-cscexe.html) 2. [找不到 roslyn\csc.exe ?!](https://blog.yowko.com/missing-roslyn-csc/) 3. [神祕的ASP.NET bin\roslyn目錄](https://blog.darkthread.net/blog/aspnet-bin-roslyn-folder/) ### How to change project url 參考文件 - [Change Project URL Visual Studio](https://stackoverflow.com/questions/20978686/change-project-url-visual-studio) [Bad Request - Invalid Hostname ASP.net Visual Studio 2015 ](https://stackoverflow.com/questions/35247847/bad-request-invalid-hostname-asp-net-visual-studio-2015) 1. Right click the project and find "Properties" ![](https://i.imgur.com/caVtfdy.png) 2. Click "Web", check and modify the Start URL ![](https://i.imgur.com/qqURrYO.png) 3. Find the path ```\.vs\config```, within that folder you will see your applicationhost.config file, then add the following code in applicationhost.config file ```config= <site name="DownloadSpeedLimit" id="2"> <application path="/" applicationPool="Clr4IntegratedAppPool"> <virtualDirectory path="/" physicalPath="D:\DownloadSpeedLimit\DownloadSpeedLimit" /> </application> <bindings> <!--important --> <binding protocol="http" bindingInformation="*:50024:192.168.1.61" /> <binding protocol="http" bindingInformation="*:50024:localhost" /> </bindings> </site> ``` 4. **Run Visual Studio as Administrator** ### Can't download file larger then 2GB As title ## 6. 未來要做的研究 1. 影片 buffer 大小限制與 ASP.NET(4.5以上), OS, 電腦環境是否有關係 2. 承上,調查 Stream read 時 default buffer 為多少