Skip to content

Instantly share code, notes, and snippets.

@TwistingTwists
Created March 11, 2023 05:01
Show Gist options
  • Save TwistingTwists/806f8d2422c14484c60c39a5979196e1 to your computer and use it in GitHub Desktop.
Save TwistingTwists/806f8d2422c14484c60c39a5979196e1 to your computer and use it in GitHub Desktop.
drm server
package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/grafov/m3u8"
)
const (
masterPlaylistName = "playlist.m3u8"
segmentPrefix = "segment_"
keyFileName = "key.key"
ivLength = 16
keyLength = 16
)
// VideoFileInfo is a struct containing information about the input video file.
type VideoFileInfo struct {
Filename string
MimeType string
FileSize int64
Duration time.Duration
Bitrate int
Width int
Height int
Framerate float64
}
// GetVideoFileInfo returns information about the input video file.
func GetVideoFileInfo(filename string) (VideoFileInfo, error) {
var info VideoFileInfo
// Get file information
fileInfo, err := os.Stat(filename)
if err != nil {
return info, err
}
info.Filename = filename
info.FileSize = fileInfo.Size()
// Get file MIME type
file, err := os.Open(filename)
if err != nil {
return info, err
}
defer file.Close()
mimeType, err := mimetype.DetectReader(file)
if err != nil {
return info, err
}
info.MimeType = mimeType.String()
// Get video information using ffprobe
cmd := exec.Command("ffprobe", "-v", "quiet", "-show_format", "-show_streams", "-print_format", "json", filename)
output, err := cmd.Output()
if err != nil {
return info, err
}
var ffprobeOutput struct {
Format struct {
Duration string `json:"duration"`
} `json:"format"`
Streams []struct {
CodecType string `json:"codec_type"`
BitRate string `json:"bit_rate"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Framerate string `json:"r_frame_rate,omitempty"`
DisplayRatio string `json:"display_aspect_ratio,omitempty"`
Language *string `json:"language,omitempty"`
} `json:"streams"`
}
err = json.Unmarshal(output, &ffprobeOutput)
if err != nil {
return info, err
}
for _, stream := range ffprobeOutput.Streams {
if stream.CodecType == "video" {
if stream.Width > 0 {
info.Width = stream.Width
}
if stream.Height > 0 {
info.Height = stream.Height
}
if stream.Framerate != "" {
framerateParts := strings.Split(stream.Framerate, "/")
if len(framerateParts) == 2 {
numerator, _ := strconv.Atoi(framerateParts[0])
denominator, _ := strconv.Atoi(framerateParts[
// 1])
info.Framerate = float64(numerator) / float64(denominator)
}
}
}
if stream.BitRate != "" {
bitrate, _ := strconv.Atoi(stream.BitRate)
info.Bitrate += bitrate
}
}
if ffprobeOutput.Format.Duration != "" {
duration, _ := strconv.ParseFloat(ffprobeOutput.Format.Duration, 64)
info.Duration = time.Duration(duration * float64(time.Second))
}
return info, nil
}
// GenerateKey generates a random key for AES-128 encryption.
func GenerateKey() ([]byte, error) {
key := make([]byte, keyLength)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, err
}
return key, nil
}
// GenerateIV generates a random IV for AES-128 encryption.
func GenerateIV() ([]byte, error) {
iv := make([]byte, ivLength)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
return iv, nil
}
// GenerateSegmentName generates the name of a segment file.
func GenerateSegmentName(i int) string {
return fmt.Sprintf("%s%06d.ts", segmentPrefix, i)
}
// GenerateMediaPlaylist generates a media playlist file for a given segment duration.
func GenerateMediaPlaylist(segmentDuration time.Duration, segmentCount int) ([]byte, error) {
playlist := m3u8.NewMediaPlaylist(segmentCount, segmentCount)
playlist.TargetDuration = uint64(segmentDuration.Seconds())
for i := 0; i < segmentCount; i++ {
segmentName := GenerateSegmentName(i)
segment := m3u8.NewMediaSegment(segmentName, float64(segmentDuration.Seconds()), 0, "")
playlist.AppendSegment(segment)
}
return playlist.Encode().Bytes(), nil
}
// GenerateMasterPlaylist generates a master playlist file containing links to all media playlists.
func GenerateMasterPlaylist(segmentCount int) ([]byte, error) {
playlist := m3u8.NewMasterPlaylist()
mediaPlaylist := m3u8.NewMediaPlaylist(segmentCount, segmentCount)
mediaPlaylistURI := "media.m3u8"
playlist.Append(mediaPlaylistURI, "video", "")
playlist.Encode()
return playlist.Encode().Bytes(), nil
}
// EncryptVideoFile encrypts a video file with EME encryption and generates a set of segment files and playlists.
func EncryptVideoFile(filename string) error {
// Get information about the input video file
videoInfo, err := GetVideoFileInfo(filename)
if err != nil {
return err
}
// Generate key and IV for encryption
key, err := GenerateKey()
if err != nil {
return err
}
iv, err := GenerateIV()
if err != nil {
return err
}
// Create output directory for encrypted files
outputDir := strings.TrimSuffix(filename, filepath.Ext(filename))
err = os.MkdirAll(outputDir, os.ModePerm)
if err != nil {
return err
}
// Create key file for encryption
keyFilePath := filepath.Join(outputDir, keyFileName)
err = ioutil.WriteFile(keyFilePath, key, os.ModePerm)
if err != nil {
return err
}
// Create media playlist
segmentDuration := time.Second * 5
segmentCount := int(videoInfo.Duration.Seconds() / segmentDuration.Seconds())
if int(videoInfo.Duration.Seconds())%int(segmentDuration.Seconds()) != 0 {
segmentCount++
}
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/grafov/m3u8"
)
const (
keyFileName = "key.bin"
segmentPrefix = "segment_"
keyLength = 16
ivLength = 16
aes128KeyURI = "key.bin"
aes128KeyFormat = "identity"
segmentEncryption = "AES-128"
)
// VideoInfo contains information about a video file.
type VideoInfo struct {
Duration time.Duration
Bitrate int
Framerate float64
}
// GetVideoFileInfo gets information about a video file using ffprobe.
func GetVideoFileInfo(filename string) (VideoInfo, error) {
var info VideoInfo
cmd := exec.Command("ffprobe", "-v", "error", "-show_format", "-show_streams", "-print_format", "json", filename)
output, err := cmd.Output()
if err != nil {
return info, err
}
ffprobeOutput := FFprobeOutput{}
err = ffprobeOutput.UnmarshalJSON(output)
if err != nil {
return info, err
}
for _, stream := range ffprobeOutput.Streams {
if stream.CodecType == "video" {
if stream.FrameRate != "" {
if strings.Contains(stream.FrameRate, "/") {
parts := strings.Split(stream.FrameRate, "/")
numerator, _ := strconv.Atoi(parts[0])
denominator, _ := strconv.Atoi(parts[1])
info.Framerate = float64(numerator) / float64(denominator)
}
}
}
if stream.BitRate != "" {
bitrate, _ := strconv.Atoi(stream.BitRate)
info.Bitrate += bitrate
}
}
if ffprobeOutput.Format.Duration != "" {
duration, _ := strconv.ParseFloat(ffprobeOutput.Format.Duration, 64)
info.Duration = time.Duration(duration * float64(time.Second))
}
return info, nil
}
// GenerateKey generates a random key for AES-128 encryption.
func GenerateKey() ([]byte, error) {
key := make([]byte, keyLength)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
return nil, err
}
return key, nil
}
// GenerateIV generates a random IV for AES-128 encryption.
func GenerateIV() ([]byte, error) {
iv := make([]byte, ivLength)
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
return iv, nil
}
// GenerateSegmentName generates the name of a segment file.
func GenerateSegmentName(i int) string {
return fmt.Sprintf("%s%06d.ts", segmentPrefix, i)
}
// GenerateMediaPlaylist generates a media playlist file for a given segment duration.
func GenerateMediaPlaylist(segmentDuration time.Duration, segmentCount int) ([]byte, error) {
playlist := m3u8.NewMediaPlaylist(segmentCount, segmentCount)
playlist.TargetDuration = uint64(segmentDuration.Seconds())
for i := 0; i < segmentCount; i++ {
segmentName := GenerateSegmentName(i)
segment := m3u8.NewMediaSegment(segmentName, float64(segmentDuration.Seconds()), 0,
fmt.Sprintf("./%s", segmentName))
segment.Encryptable = true
segment.Key = &m3u8.Key{
Method: aes128KeyFormat,
URI: aes128KeyURI,
IV: hex.EncodeToString(iv),
KeyFormat: aes128KeyFormat,
KeyFormatVersions: "1",
}
playlist.AppendSegment(segment)
}
return playlist.Encode().Bytes(), nil
}
func main() {
videoFilename := "./example.mp4"
segmentDuration := 10 * time.Second
videoInfo, err := GetVideoFileInfo(videoFilename)
if err != nil {
fmt.Println("Error getting video info:", err)
os.Exit(1)
}
// Calculate the number of segments
segmentCount := int(videoInfo.Duration / segmentDuration)
if videoInfo.Duration%segmentDuration != 0 {
segmentCount++
}
// Generate the key and IV
key, err := GenerateKey()
if err != nil {
fmt.Println("Error generating key:", err)
os.Exit(1)
}
iv, err := GenerateIV()
if err != nil {
fmt.Println("Error generating IV:", err)
os.Exit(1)
}
// Write the key to a file
err = ioutil.WriteFile(keyFileName, key, 0600)
if err != nil {
fmt.Println("Error writing key file:", err)
os.Exit(1)
}
// Generate the media playlist
mediaPlaylistBytes, err := GenerateMediaPlaylist(segmentDuration, segmentCount)
if err != nil {
fmt.Println("Error generating media playlist:", err)
os.Exit(1)
}
err = ioutil.WriteFile("playlist.m3u8", mediaPlaylistBytes, 0644)
if err != nil {
fmt.Println("Error writing media playlist:", err)
os.Exit(1)
}
// Split the video into segments
cmd := exec.Command("ffmpeg", "-i", videoFilename, "-c:v", "copy", "-c:a", "copy", "-map", "0", "-f", "segment", "-segment_time", strconv.Itoa(int(segmentDuration.Seconds())), "-segment_list", "playlist.m3u8", "-segment_format", "mpegts", "-bsf:v", "h264_mp4toannexb", "-flags", "+global_header", "-y", filepath.Join(segmentPrefix, "%06d.ts"))
err = cmd.Run()
if err != nil {
fmt.Println("Error splitting video into segments:", err)
os.Exit(1)
}
fmt.Println("Successfully generated encrypted media files.")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment