---
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 圖解 :

### 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 完,才可播放

### 影片 metadata
#### 1. type1.mp4 metadata:


#### 2. type2.mp4 metadata:


#### 3. type3.mp4 metadata:


### 傳輸流量測試
以下為在「不跳著播放影片」的條件下測試播放是否順暢
#### 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 專案後可能會遇到以下問題

請參考下列文章之解決辦法
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"

2. Click "Web", check and modify the Start URL

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 為多少