为了账号安全,请及时绑定邮箱和手机立即绑定

用Go语言实现简单的HTTP视频流服务器

标签:
Go API 直播
用 Go 实现视频直播的简短指南

2004年,我看了第一个来自互联网的视频。那是一段通过iTunes下载的音乐视频。重点是“下载”。从那时起,我们大多数人看互联网视频的方式已经发生了很大变化。从只下载,到只看流媒体。

不论是通过 YouTube 还是 Netflix 平台,最终总是在某个地方有一台服务器在为你传输你正在观看的视频。最初,我对视频流媒体的理解是:

  • 这是一件很难实施的事。
  • 它需要一些专门的协议,我太懒去学。

原来搭建一个流媒体服务器其实很简单。不过,我也不想小看那些大型视频流媒体公司所面临的挑战。

Thomas William — https://unsplash.com/photos/person-holding-video-camera-4qGbMEZb56c

在这篇文章里,我们将用 Go 搭建一个符合 RFC 7233 的 HTTP 视频流媒体服务器。

架构:

在编写任何 Go 代码之前,我想先解释视频流在现代浏览器中的工作方式。这是默认的开箱即用行为,在本文中不会涉及任何 HTML 代码。

在我看来,流式传输允许客户端请求资源的特定部分,而不是下载整个文件。客户端会发送一个带有Range头部的HTTP请求来获取文件的一部分。该头部指定了服务器应返回文件的哪个部分。服务器还会用一个名为Content-Range的响应头部告知客户端它请求了文件的哪些部分。

这里有一幅图来更清楚地说明上面提到的想法:

Go 语言的代码示例

要播放一个MP4视频文件,我需要什么呢?

  • 可以退还视频片段流的视频店。
  • 一个端点用于解析客户端请求并返回所需的文件片段。

我将从视频流播放器界面开始。这个界面将有一个功能,用于查找文件中的某些片段。下面是对 VideoStreamer 接口的定义:

    type VideoStreamer interface {  
     // Seek 方法的 start 和 end 参数都是以字节为单位的。1024 表示 1KB。  
     Seek(key string, start, end int) ([]byte,error)  
    }

接下来,我将添加一个新的 struct 类型,名为 MockVideoStreamer。此类型将有一个名为 Store 的字段,用于存储视频数据。这里是该类型的代码示例:

    // MockVideoStreamer 是一个简单的视频流模拟实现,视频流接口
    type MockVideoStreamer struct {  
     Store map[string][]byte  
    }

声明了 struct 类型之后,我将通过更新 MockVideoStreamer 类型来实现 VideoStreamer 接口的功能。Seek 方法将检查提供的视频键是否存在于 Store 字段中,如果存在,则返回请求的字节,如果不存在,则返回错误。下面是该实现的代码:

    // Seek 根据键和指定的字节范围从存储中检索视频片段
    func (m *MockVideoStreamer) Seek(key string, start, end int) ([]byte, error) {  
        // 使用键从存储中检索视频数据
        videoData, exists := m.Store[key]  
        if !exists {  
            return nil, fmt.Errorf("找不到视频")  
        }  

        // 确保范围在视频数据的边界内
        if start < 0 || start >= len(videoData) {  
            return nil, fmt.Errorf("起始字节 %d 超出视频数据范围", start)  
        }  

        if end < start || end >= len(videoData) {  
            end = len(videoData) - 1 // 超出边界时,将结束位置调整为最后一个字节
        }  

        // 返回视频数据的片段
        return videoData[start : end+1], nil  
    }

下一步是添加一个HTTP处理器,以便将所有这些优秀的代码公开到网络。

HTTP 处理程序

在这篇文章中,我将使用来自标准库中的http包里的HandlerFunc类型来进行流处理。这是一个普通的设置。HandlerFunc将通过一个工厂函数生成。该工厂函数将有三个参数。

  • streamer: 这将是 VideoStreamer 类型的对象,用于获取处理器请求的文件部分。
  • videoKey: 视频的唯一标识 videoKey。它将作为 Seek 方法的 key 参数传递。
  • totalSize: 视频的总大小。

我会把 HandlerFunc 功能分成三个部分:

  • 阅读并准备从客户端请求接收的数据。
  • 加载客户端请求的字节数据。
  • 用适当的头部信息将请求的字节回传给客户端。

在这第一部分,我的处理程序将读取Range头部的值。读取后,处理程序将提取请求字节范围的开始和结束索引。这将通过分割Range头部值并将开始和结束索引的字符串转换为int类型来完成。以下是一部分的代码:

func VideoStreamHandler(streamer VideoStreamer, videoKey string, totalSize int) http.HandlerFunc {  
   return func(w http.ResponseWriter, r *http.Request) {  

      rangeHeader := r.Header.Get("Range")  
      var start, end int  

      if rangeHeader == "" {  
         // 默认前1MB  
         start = 0  
         end = 1024*1024 - 1  
      } else {  

         // 解析Range头:"bytes=start-end"  
         rangeParts := strings.TrimPrefix(rangeHeader, "bytes=")  
         rangeValues := strings.Split(rangeParts, "-")  
         var err error  

         // 获取起始字节值  
         start, err = strconv.Atoi(rangeValues[0])  
         if err != nil {  
            http.Error(w, "无效的字节范围", http.StatusBadRequest)  
            return  
         }  

         // 获取结束字节或设置默认值  
         if len(rangeValues) > 1 && rangeValues[1] != "" {  
            end, err = strconv.Atoi(rangeValues[1])  
            if err != nil {  
               http.Error(w, "无效的字节范围", http.StatusBadRequest)  
               return  
            }  
         } else {  
            end = start + 1024*1024 - 1 // 默认1MB部分  
         }  
      }  

      // 确保结束字节不超过总视频大小  
      if end >= totalSize {  
         end = totalSize - 1  
      }  

      ...  
   }  
}

对于第二部分,我会将 videoKeystartend 变量传递给 VideoStreamerSeek 方法中以获取所需的字节流。这里是执行此操作的代码:

      func VideoStreamHandler(streamer VideoStreamer, videoKey string, totalSize int) http.HandlerFunc {  
       return func(w http.ResponseWriter, r *http.Request) {  

          ...  
          // 获取视频  
          videoData, err := streamer.Seek(videoKey, start, end)  
          if err != nil {  
             http.Error(w, fmt.Sprintf("获取视频出错: %v", err), http.StatusInternalServerError)  
             return  
          }  

          ...  
       }  
    }

一旦请求的字节被加载到内存中,我就可以进入第三部分并返回响应给客户端。首先返回一套对于流媒体工作必不可少的头部信息,不包含额外的HTML……具体如下是头部信息列表:

  • Content-Range: 指示响应中所服务文件的字节范围和总大小。
  • Accept-Ranges: 指示服务器是否支持范围请求。
  • Content-Length: 指定响应正文的长度(以字节为单位)。
  • Content-Type: 指定所服务资源的媒体类型,确保客户端(如浏览器或媒体播放器)能正确解读内容。

接下来,我会将响应状态码设置为部分内容状态码(206),并将VideoStreamer接口返回的字节写入响应体。

    func VideoStreamHandler(streamer VideoStreamer, videoKey string, totalSize int) http.HandlerFunc {  
       return func(w http.ResponseWriter, r *http.Request) {  
          ...  
          // 设置响应头并发送视频片段  
          w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, totalSize))  
          w.Header().Set("Accept-Ranges", "bytes")  
          w.Header().Set("Content-Length", fmt.Sprintf("%d", len(videoData)))  
          w.Header().Set("Content-Type", "video/mp4")  
          w.WriteHeader(http.StatusPartialContent)  

          _, err := w.Write(videoData)  
          if err != nil {  
             http.Error(w, "视频流处理出错", http.StatusInternalServerError)  
          }  
       }  
    }

您可以在下面的“源码”部分找到此处理程序的完整源码。

把它们放在一起

为了测试之前定义的那段代码,我将添加一个主函数来做以下事情:

  • 从一个素材网站下载一个 MP4 文件。并将下载的 MP4 文件存储到 MockVideoStreamer 实例中。
  • 将由 VideoStreamHandler 返回的 HandlerFunc 绑定到 /video 路由。
  • 启动一个在端口 8080 监听的 web 服务器。

以下是用来完成这些任务的代码:

# 原始代码段,保持不变
    const url = "https://download.samplelib.com/mp4/sample-5s.mp4"  

    func 主函数() {  

       // DownloadBytes 是一个工具函数。  
       // 详见下方的完整源代码  
       data, err := DownloadBytes(url)  
       if err != nil {  
          fmt.Println("下载错误:", err)  
          return  
       }  

       日志打印("数据长度:", len(data))  
       streamer := &MockVideoStreamer{  
        Store: map[string][]byte{"video-key": data},  
       }  

       http.HandleFunc("/video", VideoStreamHandler(streamer, "video-key", len(data)))  
       http.ListenAndServe(":8080", nil)  
    }

然后……这就来了:

网络已降至3G速度

总结

这篇帖子展示了如何利用客户端范围请求来提供流式传输体验。它还展示了 Go 语言开箱即用的功能。该帖子中的代码也存在一些局限性。

  • 它会把文件数据加载到内存里。
  • 正确的方法是用 io.Reader 来流式传输数据。
  • 不支持完整内容请求,仅支持部分内容。

在高流量的环境中,流媒体传输可能需要不同的配置。我并不建议在这种环境下使用上述代码,尤其是考虑到这些限制。

谢谢您的阅读。

参考资料

RFC 7233 — 这是一个关于范围请求的RFC文档 — https://datatracker.ietf.org/doc/html/rfc7233

完整代码可以在这里找到:https://gist.github.com/cheikhsimsol/12183f73b250bc09952ae52b5f36f53e

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消