Last active
July 2, 2024 05:43
-
-
Save mahmoud-eskandari/ea0cbb095133e63efe3236c24bd1f182 to your computer and use it in GitHub Desktop.
Simple Golang Mp4 to hls server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"fmt" | |
"io" | |
"math" | |
"net/http" | |
"os" | |
"path" | |
"strconv" | |
"strings" | |
"time" | |
"github.com/yapingcat/gomedia/go-mp4" | |
"github.com/yapingcat/gomedia/go-mpeg2" | |
) | |
type hlsSession struct { | |
} | |
type mp4Segment struct { | |
start uint64 | |
end uint64 | |
duration float32 | |
uri string | |
description string | |
} | |
type hlsmuxer struct { | |
mode string | |
segments []*mp4Segment | |
streamName string | |
duration int | |
} | |
func (muxer *hlsmuxer) makeM3u8() string { | |
buf := make([]byte, 0, 4096) | |
m3u := bytes.NewBuffer(buf) | |
maxDuration := 0 | |
for _, seg := range muxer.segments { | |
duration := seg.end - seg.start | |
seg.duration = float32(duration) / 1000 | |
if maxDuration < int(math.Ceil(float64(seg.duration))) { | |
maxDuration = int(math.Ceil(float64(seg.duration))) | |
} | |
} | |
m3u.WriteString("#EXTM3U\n") | |
m3u.WriteString(fmt.Sprintf("#EXT-X-TARGETDURATION:%d\n", maxDuration)) | |
m3u.WriteString("#EXT-X-VERSION:3\n") | |
m3u.WriteString("#EXT-X-MEDIA-SEQUENCE:0\n") | |
for _, seg := range muxer.segments { | |
m3u.WriteString(fmt.Sprintf("#EXTINF:%.3f,%s\n", seg.duration, seg.description)) | |
m3u.WriteString(muxer.streamName + "/" + seg.uri + "\n") | |
} | |
m3u.WriteString("#EXT-X-ENDLIST\n") | |
return m3u.String() | |
} | |
func (muxer *hlsmuxer) makeHlsSegment(table []mp4.SyncSample, endTimestamp uint64) { | |
if len(table) == 0 { | |
return | |
} | |
idx := 0 | |
start := table[idx].Dts | |
for start < table[len(table)-1].Dts { | |
if idx < len(table)-1 && table[idx].Dts-start < uint64(muxer.duration)*1000 { | |
idx++ | |
continue | |
} | |
seg := &mp4Segment{ | |
start: start, | |
end: table[idx].Dts, | |
description: fmt.Sprintf("mp4 sync sample %d", idx), | |
uri: fmt.Sprintf("sequence-%d.ts?start=%d&end=%d", len(muxer.segments), start, table[idx].Dts), | |
} | |
muxer.segments = append(muxer.segments, seg) | |
start = table[idx].Dts | |
idx++ | |
} | |
if start < endTimestamp { | |
seg := &mp4Segment{ | |
start: start, | |
end: endTimestamp, | |
description: fmt.Sprintf("last mp4 sync sample"), | |
uri: fmt.Sprintf("sequence-%d.ts?start=%d&end=%d", len(muxer.segments), start, endTimestamp), | |
} | |
muxer.segments = append(muxer.segments, seg) | |
} | |
} | |
func onM3U8(w http.ResponseWriter, r *http.Request) { | |
streamName := strings.TrimLeft(r.URL.Path, ge("URL_PREFIX", "/vod/")) | |
filePath := strings.TrimRight(streamName, ".m3u8") | |
sp := strings.Split(streamName, "/") | |
streamName = strings.TrimRight(sp[len(sp)-1], ".m3u8") | |
f, err := os.Open(path.Join(ge("DIR_PREFIX", "./"), filePath+".mp4")) | |
if err != nil { | |
fmt.Println(err) | |
return | |
} | |
defer f.Close() | |
demuxer := mp4.CreateMp4Demuxer(f) | |
headInfo, err := demuxer.ReadHead() | |
if err != nil && err != io.EOF { | |
fmt.Println(err) | |
} else { | |
fmt.Printf("%+v\n", headInfo) | |
} | |
vid := 0 | |
var endTs uint64 = 0 | |
for _, info := range headInfo { | |
if info.Cid == mp4.MP4_CODEC_H264 || info.Cid == mp4.MP4_CODEC_H265 { | |
vid = info.TrackId | |
endTs = info.EndDts | |
} | |
} | |
table, err := demuxer.GetSyncTable(uint32(vid)) | |
if err != nil { | |
fmt.Println(err) | |
} | |
muxer := hlsmuxer{duration: 10, streamName: streamName} | |
muxer.makeHlsSegment(table, endTs) | |
w.Header().Add("Content-Type", "application/vnd.apple.mpegurl") | |
w.Header().Set("Access-Control-Allow-Origin", "*") | |
w.Header().Set("Access-Control-Allow-Headers", "*") | |
w.Header().Set("Access-Control-Allow-Credentials", "true") | |
m := muxer.makeM3u8() | |
fmt.Println(m) | |
body := []byte(m) | |
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(body))) | |
w.Write(body) | |
} | |
func onTs(w http.ResponseWriter, r *http.Request) { | |
start, _ := strconv.ParseInt(r.URL.Query().Get("start"), 10, 64) | |
end, _ := strconv.ParseInt(r.URL.Query().Get("end"), 10, 64) | |
sp := strings.Split(strings.TrimLeft(r.URL.Path, ge("URL_PREFIX", "/vod/")), "/") | |
sp = sp[:len(sp)-1] | |
fileName := path.Join(ge("DIR_PREFIX", "./"), strings.Join(sp, "/")+".mp4") | |
f, err := os.Open(fileName) | |
if err != nil { | |
fmt.Println(err) | |
return | |
} | |
defer f.Close() | |
demuxer := mp4.CreateMp4Demuxer(f) | |
demuxer.ReadHead() | |
demuxer.SeekTime(uint64(start)) | |
buf := bytes.NewBuffer(make([]byte, 0, 1024*1024)) | |
muxer := mpeg2.NewTSMuxer() | |
muxer.OnPacket = func(pkg []byte) { | |
buf.Write(pkg) | |
} | |
vid := muxer.AddStream(mpeg2.TS_STREAM_H264) | |
aid := muxer.AddStream(mpeg2.TS_STREAM_AAC) | |
first := true | |
for { | |
pkg, err := demuxer.ReadPacket() | |
if err != nil { | |
w.Write([]byte(err.Error())) | |
return | |
} | |
if first && pkg.Cid == mp4.MP4_CODEC_H264 { | |
first = false | |
} | |
// | |
if pkg.Dts >= uint64(end) { | |
break | |
} | |
if pkg.Cid == mp4.MP4_CODEC_H264 { | |
muxer.Write(vid, pkg.Data, pkg.Pts, pkg.Dts) | |
} else if pkg.Cid == mp4.MP4_CODEC_AAC { | |
muxer.Write(aid, pkg.Data, pkg.Pts, pkg.Dts) | |
} | |
} | |
w.Header().Set("Content-Length", fmt.Sprintf("%d", buf.Len())) | |
w.Header().Set("Content-Type", "video/mp2t") | |
w.Header().Set("Access-Control-Allow-Origin", "*") | |
w.Header().Set("Access-Control-Allow-Headers", "*") | |
w.Header().Set("Access-Control-Allow-Credentials", "true") | |
w.Write(buf.Bytes()) | |
} | |
func onVod(w http.ResponseWriter, r *http.Request) { | |
if strings.LastIndex(r.URL.Path, "m3u8") != -1 { | |
onM3U8(w, r) | |
} else { | |
onTs(w, r) | |
} | |
} | |
func main() { | |
mux := http.NewServeMux() | |
mux.HandleFunc(ge("URL_PREFIX", "/vod/"), onVod) | |
server := http.Server{ | |
Addr: ge("ADDR", ":80"), | |
Handler: mux, | |
ReadTimeout: time.Second * 10, | |
WriteTimeout: time.Second * 10, | |
} | |
fmt.Println("server.listen", ge("ADDR", ":80")) | |
fmt.Println(server.ListenAndServe()) | |
} | |
//get env | |
func ge(name, defaultv string) string { | |
v, exists := os.LookupEnv(name) | |
if !exists || v == "" { | |
return defaultv | |
} | |
return v | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Build:
RUN:
work directory: