Created
March 11, 2023 05:01
-
-
Save TwistingTwists/806f8d2422c14484c60c39a5979196e1 to your computer and use it in GitHub Desktop.
drm server
This file contains hidden or 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 ( | |
"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++ | |
} |
This file contains hidden or 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 ( | |
"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