写点什么

依赖腾讯云的音视频服务能力,构建一个高可用的在线直播平台

作者:为自己带盐
  • 2025-03-26
    河北
  • 本文字数:8391 字

    阅读完需:约 28 分钟

你先等会儿!

这里呢,有同学会说,你都依赖腾讯云了,还构造个啥?

实际上,很多云厂商也确实有那种打包好的解决方案,对外出售,用户可能什么都不要开发,拿来开箱即用,但是,_价格不菲_!而且这种打包方案,场景都是固定的,很难进行二次开发,想定制独特功能就又是一笔不小的开销。

就算要选择打包方案,大厂也会推出很多眼花缭乱的方向和使用场景,如果没点专业知识,很难选中适合自己的那一套方案,还有就是,这种方案基本都是订阅制,按月或者按天付费,单价也很贵,感兴趣的可以到各厂官网对接销售去深入了解下。

所以如果你或你司有矿不差钱,也确实有合适的场景,或者盈利模式非常清晰,那确实是可以直接购买而不是自己开发。

但对于大部分小微企业,以及相当一部分中型企业,或者说致力于构建基于自身业务的在线直播系统,那这种打包方案可能就不是最佳方案了。

那完全脱离大厂的桎梏,全都自己开发行不行?

这又是另一条极端的路线,从技术角度来说,当然是行,但考虑到高可用,高质量的服务,对大部分开发团队来说,还是要依赖大厂的服务支撑,毕竟资源也是成本,加入你的视频平台要发布给几万,几十万甚至几百万用户使用,那除了自己去建立机房,搭建服务器集群,解决网络资源等,还要考虑自己去维护这些资源,成本就又是无边无际了。

所以,对大部分开发者来说,既不能全靠大厂,也不能没有大厂,大厂给自己定位也是这样,只提供剥离业务的基础设施和资源的运维服务,其他提倡用户自己对接。

我们常说的 SaaS,PaaS,LaaS。。。之类的,SaaS 就是类似全包的方案,花钱买省心(但往往很难真省心),其他就是少花点钱,在花点心思,最终得到一个不错的结果,所谓开发就是权衡的艺术,在这里还蛮贴切~

背景

我们的直播业务是建立在自由在线教育平台上的,属于其中一个比较大的模块,我就以这个系统为例,展开说一下接入直播服务都需要哪些关键操作。

准备工作

首先,我们要搞清楚定位,腾讯云提供的只是稳定的直播能力,录制能力等与业务不相干的服务,实际的业务还是要在对接之前就已经构建完成了。


所以我们的准备工作包括

业务系统

我们需要一个即便脱离了云服务,也能稳定运行的业务系统。比如我这里除了直播和拉取回放的时候需要对接云服务,其他业务,包括课程管理,用户管理,专家管理,专题管理,评论打卡,数据分析等等,都是在接入云服务之前就已经开发完成的。

准备域名

对接直播服务,需要准备一个推流域名和一个拉流域名,另外需要做好一些关键配置,这些云厂商的开发文档里介绍的都很详细,以文档为准就好;https://cloud.tencent.com/document/product/267/13551

备好 Money

域名对接好之后,就可以在云账户里充点米了,如果是新开通的服务,官方给赠送一些流量供测试,上了业务以后,还是要自己购买流量包或者根据实际情况后付费。


具体还是参照文档说明即可https://cloud.tencent.com/document/product/267/52662


需要说明的是,即便是我们提前购买了流量包,账户里还是要预留一些资金,不用多,但得有,因为服务启动之后,除了常规的直播流量,还会产生一些其他按天扣费的费用,如果账户余额为 0,搞不好会被停服。


域名对接

域名接入之后,要进行一系列的配置,还是推荐参照文档操作即可。


这里重点聊一下安全性的问题,推荐的做法是推拉流域名都要开启鉴权,且在拉流端除了域名鉴权,还要配置好本地的 token 服务来完成域名的鉴定,避免流量被盗刷。


以推流为例,我们要开启推流鉴权



然后生成推流地址时,除了可以在腾讯云的控制台生成,也可以集成到我们自己的代码里,方便管理。


由于这块的接入比较简单,官方文档里只提供了 Java,Go 和 PHP 的接入案例,其他参照完成就可以。我这里是一个 C#版本


public string GetSafePushUrl(string streamName, long txTime=0){    if (string.IsNullOrEmpty(streamName))        return "error";    var config = _cloudConfigFactory.GetConfigByPurpose("live", "tencent");        LiveParamsModel model = JsonHelper.SafeDeserialize<LiveParamsModel>(config.Other);    if (txTime == 0)    {        DateOnly dto = DateOnly.FromDateTime(DateTime.Now);        TimeOnly tmo = new TimeOnly(23, 59, 59);        txTime = Utils.GetUnixTimestamp(new DateTime(dto, tmo, DateTimeKind.Utc));    }    string input = $"{model.pushKeyMain}{streamName}{txTime:X}";
string txSecret = Security.ByteArrayToHexString(Security.MD5Hash(input));
string url = $"{model.protocol}//{model.pushUrl}/{model.appName}/{streamName}?txSecret={txSecret}&txTime={txTime:X}"; Logger.Debug("推流地址:"+url); return url;}
复制代码

直播转码

因为我们的直播场景有那种需要和线下导播对接的时候,需要我们提供推流地址,但导播的推流规格一般不固定,有的高,有的很高,比如把码率推到 6000Kbps 以上,那直播的时候如果原样拉流,流量包买多少也不够用!


所以,为了避免出现流量刺客的情况,要配置好几个统一的转码模板。



注意,配置了转码模板后,再购买直播相关服务的时候,要购买转码包,这个不贵,按小时计费,很合适。另外配置转码模板之后,生成拉流地址的时候也要指定好转码模板,这个大家参照官方文档说明操作即可。

直播录制

直播录制也是非常重要的增值功能,直播结束后我们可以方便的获取回放文件



需要说明的是,如果我们的回放文件需要在本地进行剪辑,我们还有额外开发一个下载回放的程序,不然每次都要手动去下载,而录制存储的文件名称一般都比较长,文件少还好,多了以后,三头六臂也下载不过来。



这里,官方也有对接下载文件的代码,我这里也提供一个 C#的版本


下载回放资源一般至少要分 2 步,第一是根据 streamid 搜索媒资,代码如下


public static async Task<MediaInfo[]> SearchMedia(string streamId){    try    {        AnsiConsole.MarkupLine($"[#FDE047]搜索vod资源...{DateTime.Now}[/]");        Credential cred = new Credential        {            SecretId = TencentVodSettings.SecretId,            SecretKey = TencentVodSettings.SecretKey        };        // 实例化一个client选项,可选的,没有特殊需求可以跳过        ClientProfile clientProfile = new ClientProfile();        // 实例化一个http选项,可选的,没有特殊需求可以跳过        HttpProfile httpProfile = new HttpProfile();        httpProfile.Endpoint = ("vod.tencentcloudapi.com");        clientProfile.HttpProfile = httpProfile;
// 实例化要请求产品的client对象,clientProfile是可选的 VodClient client = new VodClient(cred, "", clientProfile); // 实例化一个请求对象,每个接口都会对应一个request对象 SearchMediaRequest req = new SearchMediaRequest(); req.Offset = 0; req.Limit = 100; req.StreamId = streamId; req.Filters = new string[] { "basicInfo" }; // 返回的resp是一个SearchMediaResponse的实例,与请求对象对应 SearchMediaResponse resp = await client.SearchMedia(req); // 输出json格式的字符串回包 //Console.WriteLine(AbstractModel.ToJsonString(resp)); AnsiConsole.MarkupLine($"[cyan]共计搜索到{resp.MediaInfoSet.Length}条媒资记录...{DateTime.Now}[/]");
return resp.MediaInfoSet; } catch (Exception e) { AnsiConsole.MarkupLine($"[red]搜索vod资源失败:{e.Message}...{DateTime.Now}[/]"); } return Array.Empty<MediaInfo>();}
复制代码


第二步是获取真正的下载链接


/// <summary>/// 获取下载链接/// </summary>/// <param name="mediaUrls"></param>/// <returns></returns>public static string[] GetDownloadUrl(string[] mediaUrls,string streamId, long expiredAt = 0, string ext = "flv"){    AnsiConsole.MarkupLine($"[#FDE047]2.生成下载链接,共计{mediaUrls.Length}条记录...{DateTime.Now}[/]");    List<string> urls = new List<string>();    int cnt = 1;    StringBuilder contentBuilder = new StringBuilder();    foreach (string mediaUrl in mediaUrls)    {        //续命2天        //long timeStamp = Convert.ToInt64((DateTime.Now.AddDays(2) - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds);        if(expiredAt==0)            expiredAt = UtilsHelper.GetUnixTimestamp(DateTime.Now.AddDays(2));        string[] parts = mediaUrl.Split('/');        string dir = "/";        foreach (string part in parts)        {            if (part.Contains(".") || part.Contains("/") || part.Contains(":") || string.IsNullOrEmpty(part))                continue;            dir += $"{part}/";        }        //16进制Unix时间戳        string t = Convert.ToString(expiredAt, 16).ToLower().PadLeft(8, '0');        string us = UtilsHelper.GenerateRandomCodePro(10);        string sign = UtilsHelper.Md5(TencentVodSettings.UrlKey + dir + t + us);        string downloadUrl = $"{mediaUrl}?download_name={streamId}_{cnt}.{ext}&t={t}&us={us}&sign={sign}";        urls.Add(downloadUrl);        AnsiConsole.MarkupLine($"  [#20a162]--链接{cnt}:{downloadUrl}[/]");                contentBuilder.Append("{").Append($"\"FileName\":\"{streamId}_{cnt}.{ext}\",\"Url\":\"{downloadUrl}\",FolderPath:\"\"").Append("},");        cnt++;    }    return urls.ToArray();}    
复制代码


注意,涉及到验签操作,需要我们在cam那里配置密钥


到这里,就可以根据实际情况去下载了,下载的代码我这里就不给出了,不然这一篇文章写不完~


需要说明的是,不论我们的回放是放到 vod 还是 cos,这都是有一定空间的,肯定不能无限存储,所以最好还是对接一个文件管理的接口,当我们把回放文件下载到本地以后,就可以释放线上的存储了,释放方式有多种,比如归档,或者定期删除等等,我这里是选择的定期删除,对接代码如下


public static async Task<bool> ExpiredMediaInfo(string fileId, string ExipreDataISO){    if (string.IsNullOrEmpty(ExipreDataISO))    {        // 创建一个DateTimeOffset对象,设置为UTC时间        DateTimeOffset nowOffset = DateTimeOffset.UtcNow.AddDays(7);
ExipreDataISO = nowOffset.ToString("o"); } try { AnsiConsole.MarkupLine($"[#FDE047]修改vod资源过期时间...{DateTime.Now}[/]"); Credential cred = new Credential { SecretId = TencentVodSettings.SecretId, SecretKey = TencentVodSettings.SecretKey }; // 实例化一个client选项,可选的,没有特殊需求可以跳过 ClientProfile clientProfile = new ClientProfile(); // 实例化一个http选项,可选的,没有特殊需求可以跳过 HttpProfile httpProfile = new HttpProfile(); httpProfile.Endpoint = ("vod.tencentcloudapi.com"); clientProfile.HttpProfile = httpProfile; // 实例化要请求产品的client对象,clientProfile是可选的 VodClient client = new VodClient(cred, "", clientProfile); // 实例化一个请求对象,每个接口都会对应一个request对象 ModifyMediaInfoRequest req = new ModifyMediaInfoRequest(); req.FileId = fileId; req.ExpireTime = ExipreDataISO; ModifyMediaInfoResponse resp = await client.ModifyMediaInfo(req); AnsiConsole.MarkupLine($"[green]修改vod资源信息成功,已将其设为7天后过期...{DateTime.Now}[/]"); return true; } catch (Exception e) { AnsiConsole.MarkupLine($"[red]修改vod资源信息失败:{e.Message}...{DateTime.Now}[/]"); } return false;}
复制代码


每次执行完下载操作后,调用该接口,就会把线上的文件设定为 3 个月后自动过期,省去了手动处理的烦恼。


录制文件的处理,其实相对还是比较复杂的,我这里并没有把这部分的业务放到主系统里,而是单独开发了一个服务,可以完成一系列的下载,转码,上传等服务,之前也写过相关的博客,这里不在多说(传送门:https://xie.infoq.cn/article/9a8abebacf858783c67166623

直播回调

回调服务,是云服务和我们本地系统直接对接的一个主要渠道,当我们启动推流,断流,完成录制等操作时,云厂商都会以固定的形式将事件详情发送到我们业务系统。


文档地址:https://cloud.tencent.com/document/product/267/32744


实际上,我们并不需要把每一个回调都对接上,把关键的几个接入好就可以了;

推拉流回调

这个是基础事件,需要对接,可以实时的接收到推拉流的情况;


代码如下


[HttpPost][AllowAnonymous]public async Task<IActionResult> CssPushCallback([FromBody] CallbackRequestModel model){    Logger.Warning($"推拉流回调:CssPushCallback: {JsonConvert.SerializeObject(model)}");    string sign = Security.GenerateMD5Hash("magicexam" + model.t);    if (model.sign != sign)    {        Logger.Error("签名验证失败," + sign);        return Json(_resp.error("我靠,你谁啊!"));    }    Logger.Info("签名验证通过," + sign);    try    {                await RecordCssPushInfo(model);    }    catch (Exception ex)    {
Logger.Error("记录推流信息失败" + ex.Message); } return Json(_resp.success("success"));}
复制代码


注意,暴露的接口要保证腾讯云可以直接访问到,所以我们在接口方法上制定了[AllowAnonymous]属性,但同时需要注意在外层过滤一些常见的风险,比如频繁请求,危险参数,还要在接口内验证签名是否是来自腾讯云的请求,验证通过后,就可以记录业务数据了。


控制台收到接口请求的效果如下:


录制文件回调

这个和推拉流回调的方式差不多


[AllowAnonymous][HttpPost]public async Task<IActionResult> CssRecordCallback([FromBody] CallbackRecordModel model){    Logger.Warning($"录制回调:CallbackRecordModel: {JsonConvert.SerializeObject(model)}");    string sign = Security.GenerateMD5Hash("magicexam" + model.t);    if (model.sign != sign)    {        Logger.Error("签名验证失败," + sign);        return Json(_resp.error("我靠,你谁啊!"));    }    Logger.Info("签名验证通过," + sign);    try    {        await RecordCssRecordInfo(model);    }    catch (Exception ex)    {
Logger.Error("记录录制信息是失败:" + ex.Message); } await _redisProvider.StringSetAsync($"{model.stream_id}_css_record_callback", JsonConvert.SerializeObject(model), TimeSpan.FromMinutes(30)); return Json(_resp.success("success"));}
复制代码


同样,也是暴露为开放接口,并在接口内完成验证签名的操作。


服务台收到的效果如下:


* CDN 加速

这其实不是直播范畴了,但一个完整的直播课服务,除了直播环节,另外一个重要的环节就是回放。


实际上,我们可以使用腾讯 vod 的能力,来分发回放视频,只需要在云平台做好一些配置就好。


而前面也提到了,Vod 的资源不可能一直有效,存储空间也不是无限大的,而且有些时候,不是开发者能决定要把回放文件放在哪里的,决策层可能觉得还是要在本地保管跟放心,那我们本地的带宽资源又有限,这时候就可以搭配 cdn 完成静态资源的边缘加速,让回放课程的播放也更加流畅。


这部分,云端的操作就是购买流量包,绑定域名,做好一些访问控制,并配置好回源策略等,这些参照文档操作即可。



本地需要对应的可以方便的把本地资源设置成加速资源。我这里的处理方式是,配置加速时,要求设置一个过期时间,避免长期加速造成流量被刷爆。代码如下


/// <summary>/// 加速视频/// </summary>/// <param name="liveId"></param>/// <param name="minutes">加速的分钟数,默认360分钟(6小时)</param>/// <returns></returns>[HttpPost,ValidateAntiForgeryToken]public async Task<IActionResult> TurboTs(string liveId,int minutes=360){    var live = await _context.CourseLive.Where(u => u.LiveID == Guid.Parse(liveId)).FirstOrDefaultAsync();        if (string.IsNullOrEmpty(live.FileAddress))        return Json(_resp.ret(-1, "加速失败,回放文件为空"));
string originAddress = live.FileAddress.ToLower(); string turboAddress = ""; if (!originAddress.EndsWith("m3u8")) return Json(_resp.ret(-1, "不能加速非HLS协议的回访文件")); if (originAddress.StartsWith("http") && originAddress.Contains(Common.ConfigurationHelper.GetSectionValue("turboHost"))) { return Json(_resp.ret(0, "已加速")); } if (originAddress.StartsWith("http")) { return Json(_resp.ret(-1, "不能加速外链文件")); } turboAddress = Common.ConfigurationHelper.GetSectionValue("turboHost") + originAddress; live.FileAddress = turboAddress; live.Updated_at = DateTime.Now; _context.CourseLive.Update(live); DateTime expireTime = DateTime.Now.AddMinutes(minutes); await RedisConfigure.db.HashSetAsync("turboFiles", live.LiveID.ToString(), $"{expireTime}|{originAddress}|{turboAddress}"); await _context.SaveChangesAsync(); return Json(_resp.success(turboAddress));}
/// <summary>/// 取消加速视频/// </summary>/// <param name="liveId"></param>/// <returns></returns>[HttpPost, ValidateAntiForgeryToken]public async Task<IActionResult> CancleTurbo(string liveId){ try { if (!await RedisConfigure.db.HashExistsAsync("turboFiles", liveId)) { return Json(_resp.ret(-1, "视频未加速或加速时长已过期,无需取消")); } var live = await _context.CourseLive.Where(u => u.LiveID == Guid.Parse(liveId)).FirstOrDefaultAsync(); if (string.IsNullOrEmpty(live.FileAddress)) { return Json(_resp.ret(-1, "无回放文件")); } live.FileAddress = live.FileAddress.ToLower().Replace(Common.ConfigurationHelper.GetSectionValue("turboHost"), ""); live.Updated_at = DateTime.Now; await RedisConfigure.db.HashDeleteAsync("turboFiles", liveId); _context.CourseLive.Update(live); await _context.SaveChangesAsync(); return Json(_resp.success(live.FileAddress, "操作成功")); } catch (Exception ex) { NLogUtil.fileLogger.Error($"取消加速失败,{ex.Message}||{ex.StackTrace}"); return Json(_resp.ret(-1, $"取消加速失败,{ex.Message}||{ex.StackTrace}")); }}
复制代码


Redis 的作用只是记录加速的课程和过期时间,每隔 1 小时检测一次,过期之后就取消加速了;


效果如下




这部分笔者之前也单独出过一篇博客:传送门👉:https://blog.csdn.net/juanhuge/article/details/144692801?spm=1011.2415.3001.5331

实时音视频

这部分也是我们系统里的一个模块,和直播相关的业务就是有一个混流的操作,把线上房间的课程进行云端混流,转发到直播平台,截图如下:



笔者之前曾单独写过一篇关于这部分的博客,传送门:https://blog.csdn.net/juanhuge/article/details/127983710?spm=1011.2415.3001.5331


本篇受篇幅限制,不在赘述。

结语

至此,需要开发的任务量基本完成。那这套架构真的稳定吗?我前面放过的一些地址里有一些数据展示,这几年运营下来,我们这个系统部署了 3 个节点,此外还有很多子服务,均为分布式的部署形式,日均访问量最高曾达到 300 万次,当然早期因为架构不成熟,也经常崩溃,但这两年已经很少因为系统不稳定而造成崩溃了,真正实现了我们这个规模下的,高可用,高并发,高性能。


发布于: 3 小时前阅读数: 26
用户头像

学着写代码 2019-04-11 加入

是一枚,热爱技术,天赋不高,又有点轴,的猿。。

评论

发布
暂无评论
依赖腾讯云的音视频服务能力,构建一个高可用的在线直播平台_腾讯云_为自己带盐_InfoQ写作社区