公司的APP其中一個功能是影片上傳及串流的服務,這陣子比較了許多影片串流及直播的通訊協定,最後選擇使用HLS協定來提供影片串流服務,流程包括「建立影片上傳API」、「影片轉檔」及「建立串流API」,這篇筆記記錄如何使用 ASP.NET Core Web API 來建立影音串流服務。
檔案結構
|-- Example
| |--Program.cs #程式進入點
| |
| |--Example.csproj #CSharp專案檔
| |
| |--ApiControllers #存放API
| | |--MultimediaController.cs #提供多媒體上傳及下載的API
| |
| |--Models #資料表模型存放資料夾
| | |--Multimedia.cs #用於存放影音資訊的模型
| |
| |--Services #服務存放資料夾
| | |--MultimediaService.cs #提供多媒體服務
| | |--FileService.cs #提供檔案操作服務
| | |--HLSService.cs #提供HLS串流服務
| |
| |--FFMpeg #FFMpeg可執行檔(官網下載)
|
|-- Example.sln #專案
影片上傳
建立上傳影片服務
上傳影片會用到IFormFile
這個介面,IFormFile
用來處理或儲存檔案,我們在Services
目錄底下的MultimediaService.cs
檔案裡新增CreateMultimedia
函式並傳入型別為IFormFile
的參數。
首先取得檔案的副檔名,同時產生一個新檔名,接著建立一個Multimedia的物件,將資訊存放進去。
這裡我們使用注入方式將資料庫注入,已建立好的Multimedia物件使用被注入的_dbContext加入到資料庫中,並且呼叫SaveChanges保存變動。
//MultimediaService.cs
public class MultimediaService
{
private readonly DBContext _dbContext;
public MultimediaService(DBContext dbContext)
{
//注入資料庫Context,用於對資料庫進行CRUD
_dbContext = dbContext;
}
public void CreateMultimedia(IFormFile file)
{
//取得副檔名
var fileExt = Path.GetExtension(file.FileName);
//產生一個新隨機檔名,避免檔名重複
var fileNewName = Path.GetRandomFileName();
//產生一個Multimedia物件,呼叫Add方法加入Multimedia資料表,
//最後使用_dbContext.SaveChanges()儲存更動
Multimedia multimedia = new Multimedia()
{
Uid = Guid.NewGuid(),
Type = file.ContentType,
Size = file.Length,
Ext = fileExt,
Name = fileNewName,
RecordId = request.RecordId
};
var mmedia = _dbContext.Multimedia.Add(multimedia);
_dbContext.SaveChanges();
}
}
這個檔案先寫到這,我們先建立資料儲存及轉檔的程式,接著才會把所有服務都串起來。
除了在資料庫存放檔案資訊外,還需要在實體目錄中存放實體影音檔案提供串流及下載,我們在Services
目錄底下新增FileService.cs
,用於提供檔案儲存及讀取的操作。
WriteFile
函式傳入要寫入到本機的檔案、檔案名稱及副檔名,檢查目的目錄是否存在,並使用CopyToAsync及Stream寫入檔案。
//FileService.cs
public class FileService
{
//設定檔案存放位置
private readonly string basePath = "./MultimediaFiles/";
public async Task WriteFile(IFormFile file, string fileName, string fileExt)
{
//使用Path.Combine合併多個路徑,避免多餘的'/'符號產生
var filePath = Path.Combine(basePath, fileName);
//判斷目標目錄是否存在,不存在便創建目錄
if (Directory.Exists(basePath) == false)
{
Directory.CreateDirectory(basePath);
}
//建立檔案並指定給stream
Stream stream = File.Create(filePath + fileExt);
//使用IFormFile的CopyToAsync方法,將file放入stream
await file.CopyToAsync(stream);
//關閉stream
stream.Close();
}
}
此時我們已經能夠將檔案寫入本機目錄中了,接著便是我們今天的重頭戲-轉檔
影片轉檔為m3u8格式
不免俗的我們再來建立一個服務
在Services
目錄底下新增HLSService.cs
,用於提供HLS的轉檔服務。
這裡使用FFMpeg進行轉檔,使用FFMpeg進行轉檔除了要使用NuGet安裝FFMpegCore
Package之外,還要去官網下載FFMpeg的可執行檔,放入專案目錄中。
建立一個WriteFile函式,在函式內使用FFMpegArguments
類別進行設定及轉檔。
FromFileInput
可以用於指定輸入檔案的路徑
OutputToFile
可以用於指定輸出檔案路徑
ProcessAsynchronously
用於執行轉檔
//HLSService.cs
using FFMpegCore;
public class HLSService
{
private readonly string basePath = "./MultimediaFiles/";
public async Task WriteFile(string fileName, string fileExt)
{
//判斷目標目錄是否存在,不存在便創建目錄
var filePath = Path.Combine(basePath, fileName);
if (Directory.Exists(basePath) == false)
{
Directory.CreateDirectory(basePath);
}
await FFMpegArguments
.FromFileInput(filePath + fileExt)//輸入路徑後方必須包含原檔的副檔名
.OutputToFile(filePath + ".m3u8")//輸出路徑後方必須包含".m3u8"副檔名
.ProcessAsynchronously();
}
}
到這邊已經可以將影片檔轉為m3u8及ts檔了,接著就是要把剛才的所有步驟串起來,我們把剛剛的步驟都串到MultimediaService
裡的CreateMultimedia
函式裡面。
包裝上傳流程
剛才在FileService
建立了WriteFile
方法,用於將檔案寫入目錄中,在HLSService
也建立了WriteFile
方法,用於進行影片轉檔,我們接著要把這些服務串起來。
目前MultimediaService
中的CreateMultimedia
函式寫到新增影片資訊到資料庫,接著_dbContext.SaveChanges();
的後面加入寫檔及轉檔的服務。
//MultimediaService.cs
public class MultimediaService
{
private readonly DBContext _dbContext;
private readonly FileService _file;
private readonly HLSService _hls;
public MultimediaService(DBContext dbContext, FileService file, HLSService hls)
{
_dbContext = dbContext;
_file = file;
_hls = hls;
}
//注意必須使用async將此函式指定為非同步,
//因為_file.WriteFile及_hls.WriteFile函式都是非同步方法,必須被等待
//除了加入async以外也必須加入Task,Task代表不會回傳值且以非同步方式執行函式
//若使用void,函式的呼叫端無法加上await,會導致呼叫端無法攔截例外
//容易造成靈異現象
public async Task CreateMultimedia(IFormFile file)
{
var fileExt = Path.GetExtension(file.FileName);
var fileNewName = Path.GetRandomFileName();
Multimedia multimedia = new Multimedia()
{
Uid = Guid.NewGuid(),
Type = file.ContentType,
Size = file.Length,
Ext = fileExt,
Name = fileNewName
};
var mmedia = _dbContext.Multimedia.Add(multimedia);
_dbContext.SaveChanges();
//將檔案儲存在本機
//使用DI注入的FileService服務,呼叫WriteFile函式
//參數傳入檔案、檔名及副檔名
await _file.WriteFile(file, fileNewName, fileExt);
//將檔案轉為m3u8及ts檔
//傳入檔名及副檔名
await _hls.WriteFile(fileNewName, fileExt);
}
}
恭喜我們終於完成上傳了,接著只要在API中提供此服務即可
建立上傳影片API
上傳影片的API我們把他放在ApiControllers
目錄中,在目錄中建立MultimediaController.cs
檔案存放API
//MultimediaController.cs
[Route("api/[controller]")]
[ApiController]
public class MultimediaController : Controller
{
private MultimediaService _mmedia;
public MultimediaController(MultimediaService mmedia)
{
_mmedia = mmedia;
}
[HttpPost]
public async Task<IActionResult> PostMultimedia([FromForm] IFormFile file)
{
if (file.File.ContentType.Contains("video") == false)
{
return BadRequest();
}
//呼叫上傳影片服務
await _mmedia.CreateMultimedia(file);
return Ok();
}
}
影片上傳終於告一段落了,接著是影片下載及串流
影片串流
剛才已經把影片都轉檔好了,其實影片串流就只是開一個API讓前端丟請求過來拿檔案而已…嗎? 也沒這麼單純,我們要先從資料庫取得影片列表,我們需要裡面的檔案名稱,存取完資料庫還要開一個讀檔案的服務從目錄中讀取檔案,不過這應該比剛剛的簡單多了。
從資料庫取得影片資訊
在MultimediaService.cs
中加入GetMultimediaViews
函式,取得_dbContext
中Multimedia
資料表的資料。
//MultimediaService.cs
public class MultimediaService
{
private readonly DBContext _dbContext;
private readonly FileService _file;
private readonly HLSService _hls;
public MultimediaService(DBContext dbContext, FileService file, HLSService hls)
{
_dbContext = dbContext;
_file = file;
_hls = hls;
}
public List<Multimedia> GetMultimediaViews()
{
return _dbContext.Multimedia.ToList();
}
public async Task CreateMultimedia(IFormFile file)
{
...
}
}
開好服務後由MultimediaController
呼叫GetMultimediaViews
服務,將資料表中的影音資訊清單返回給使用者
//MultimediaController.cs
[Route("api/[controller]")]
[ApiController]
public class MultimediaController : Controller
{
private MultimediaService _mmedia;
public MultimediaController(MultimediaService mmedia)
{
_mmedia = mmedia;
}
[HttpGet]
[Route("List")]
public IActionResult GetMultimediaList()
{
List<Multimedia> list = _mmedia.GetMultimediaViews();
return Ok(list);
}
[HttpPost]
public async Task<IActionResult> PostMultimedia([FromForm] IFormFile file)
{
...
}
}
從目錄取得檔案
先前寫入檔案時把程式寫在FileService.cs
,讀取檔案我們一樣把函式放在這個檔案裡面
//FileService.cs
public class FileService
{
private readonly string basePath = "./MultimediaFiles/";
public Stream? ReadFile(string fileName)
{
string filePath = Path.Combine(basePath, fileName);
if (File.Exists(fileFullPath) == false)
{
return null;
}
//使用FileStream讀取並返回檔案Stream
return new FileStream(filePath, FileMode.Open, FileAccess.Read,
FileShare.ReadWrite, 4096, FileOptions.SequentialScan);
}
public async Task WriteFile(IFormFile file, string fileName, string fileExt)
{
...
}
}
接著在MultimediaService
中呼叫剛剛建立的讀取檔案函式,建立GetMultimedia
服務
//MultimediaService.cs
public class MultimediaService
{
private readonly DBContext _dbContext;
private readonly FileService _file;
private readonly HLSService _hls;
public MultimediaService(DBContext dbContext, FileService file, HLSService hls)
{
_dbContext = dbContext;
_file = file;
_hls = hls;
}
public Stream GetMultimedia(string filename)
{
return _file.ReadFile(filename);
}
public List<Multimedia> GetMultimediaViews()
{
...
}
public async Task CreateMultimedia(IFormFile file)
{
...
}
}
到這邊已經完成所有服務了,接著只要建立取得檔案的API即可
建立串流API
在MultimediaController
中建立GetHLSMultimedia
API函式,函式中呼叫MultimediaService
中的GetHLSMultimedia
方法,取得stream後使用File
函式將stream返回給使用者,並指定Content-Type為video/mp2t
。
//MultimediaController.cs
[Route("api/[controller]")]
[ApiController]
public class MultimediaController : Controller
{
private MultimediaService _mmedia;
public MultimediaController(MultimediaService mmedia)
{
_mmedia = mmedia;
}
[HttpGet]
[Route("List")]
public IActionResult GetMultimediaList()
{
...
}
[HttpGet]
[Route("HLS/{FileName}")]
public async Task<IActionResult> GetHLSMultimedia(string FileName)
{
Stream? stream = await _mmedia.GetHLSMultimedia(FileName);
if (stream == null)
{
return NotFound();
}
return File(stream, "video/mp2t");
}
[HttpPost]
public async Task<IActionResult> PostMultimedia([FromForm] IFormFile file)
{
...
}
}
呼叫API
由於我們建立的是HttpGet方法,在瀏覽器網址列可以直接輸入網址呼叫API,在MultimediaController
設定的路由是/api/Multimedia/HLS/<已轉檔後的檔名>
,以檔名為vxvi0clz.4od
的檔案為例,剛剛在轉檔的時候會轉出一個vxvi0clz.4od.m3u8
檔案及vxvi0clz.4od0.ts
,vxvi0clz.4od1.ts
…等多個ts檔案,存取時呼叫/api/Multimedia/HLS/vxvi0clz.4od.m3u8
即可取得檔案。