Add support for proxying multicast streams through a UDPxy server.

This commit adds support for proxying multicast streams through a UDPxy
server. If the stream is multicast, and a udpxy server is set in the
configuration, then the channel URL is rewritten to use the UDPxy
service configured.

The stream URL rewriting is done regardless of the buffer option set.

Signed-off-by: 5Ub-Z3r0 <1673590+5Ub-Z3r0@users.noreply.github.com>
This commit is contained in:
5Ub-Z3r0
2020-05-12 21:29:46 +02:00
parent 2d10fc9313
commit 67b7ba6df9
13 changed files with 119 additions and 70 deletions

View File

@@ -20,7 +20,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
var settingsCategory = new Array(); var settingsCategory = new Array();
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"));
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api")); settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"));
function showPopUpElement(elm) { function showPopUpElement(elm) {

View File

@@ -305,6 +305,17 @@ var SettingsCategory = /** @class */ (function () {
setting.appendChild(tdLeft); setting.appendChild(tdLeft);
setting.appendChild(tdRight); setting.appendChild(tdRight);
break; break;
case "udpxy":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.udpxy.title}}" + ":";
var tdRight = document.createElement("TD");
var input = content.createInput("text", "udpxy", data);
input.setAttribute("placeholder", "{{.settings.udpxy.placeholder}}");
input.setAttribute("onchange", "javascript: this.className = 'changed'");
tdRight.appendChild(input);
setting.appendChild(tdLeft);
setting.appendChild(tdRight);
break;
} }
return setting; return setting;
}; };
@@ -386,6 +397,9 @@ var SettingsCategory = /** @class */ (function () {
case "xepg.replace.missing.images": case "xepg.replace.missing.images":
text = "{{.settings.replaceEmptyImages.description}}"; text = "{{.settings.replaceEmptyImages.description}}";
break; break;
case "udpxy":
text = "{{.settings.udpxy.description}}";
break;
default: default:
text = ""; text = "";
break; break;

View File

@@ -183,52 +183,52 @@
"active": { "active": {
"title": "Active", "title": "Active",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"channelName": { "channelName": {
"title": "Channel Name", "title": "Channel Name",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"description": { "description": {
"title": "Channel Description", "title": "Channel Description",
"placeholder": "Used by the Dummy as an XML description", "placeholder": "Used by the Dummy as an XML description",
"description": "" "description": ""
}, },
"updateChannelName": { "updateChannelName": {
"title": "Update Channel Name", "title": "Update Channel Name",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"channelLogo": { "channelLogo": {
"title": "Logo URL", "title": "Logo URL",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"updateChannelLogo": { "updateChannelLogo": {
"title": "Update Channel Logo", "title": "Update Channel Logo",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"epgCategory": { "epgCategory": {
"title": "EPG Category", "title": "EPG Category",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"m3uGroupTitle": { "m3uGroupTitle": {
"title": "Group Title (xteve.m3u)", "title": "Group Title (xteve.m3u)",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"xmltvFile": { "xmltvFile": {
"title": "XMLTV File", "title": "XMLTV File",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
}, },
"xmltvChannel": { "xmltvChannel": {
"title": "XMLTV Channel", "title": "XMLTV Channel",
"placeholder": "", "placeholder": "",
"description": "" "description": ""
} }
}, },
"users": { "users": {
@@ -332,6 +332,11 @@
"info_vlc": "VLC connects to the streaming server" "info_vlc": "VLC connects to the streaming server"
}, },
"udpxy": {
"title": "UDPxy address",
"description": "The address of your UDPxy server. If set, and the channel URLs in the m3u is multicast, xTeVe will rewrite it so that it is accessed via the UDPxy service.",
"placeholder": "host:port"
},
"ffmpegPath": { "ffmpegPath": {
"title": "FFmpeg Binary Path", "title": "FFmpeg Binary Path",
"description": "Path to FFmpeg binary.", "description": "Path to FFmpeg binary.",

View File

@@ -15,7 +15,7 @@ var System SystemStruct
var WebScreenLog WebScreenLogStruct var WebScreenLog WebScreenLogStruct
// Settings : Inhalt der settings.json // Settings : Inhalt der settings.json
var Settings SettingsStrcut var Settings SettingsStruct
// Data : Alle Daten werden hier abgelegt. (Lineup, XMLTV) // Data : Alle Daten werden hier abgelegt. (Lineup, XMLTV)
var Data DataStruct var Data DataStruct

View File

@@ -15,7 +15,7 @@ import (
) )
// Einstellungen ändern (WebUI) // Einstellungen ändern (WebUI)
func updateServerSettings(request RequestStruct) (settings SettingsStrcut, err error) { func updateServerSettings(request RequestStruct) (settings SettingsStruct, err error) {
var oldSettings = jsonToMap(mapToJSON(Settings)) var oldSettings = jsonToMap(mapToJSON(Settings))
var newSettings = jsonToMap(mapToJSON(request.Settings)) var newSettings = jsonToMap(mapToJSON(request.Settings))
@@ -408,7 +408,7 @@ func deleteLocalProviderFiles(dataID, fileType string) {
} }
// Filtereinstellungen speichern (WebUI) // Filtereinstellungen speichern (WebUI)
func saveFilter(request RequestStruct) (settings SettingsStrcut, err error) { func saveFilter(request RequestStruct) (settings SettingsStruct, err error) {
var filterMap = make(map[int64]interface{}) var filterMap = make(map[int64]interface{})
var newData = make(map[int64]interface{}) var newData = make(map[int64]interface{})

View File

@@ -86,6 +86,7 @@ func ShowSystemInfo() {
fmt.Println("Settings [Streaming]") fmt.Println("Settings [Streaming]")
fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer)) fmt.Println(fmt.Sprintf("Buffer: %s", Settings.Buffer))
fmt.Println(fmt.Sprintf("UDPxy: %s", Settings.UDPxy))
fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize)) fmt.Println(fmt.Sprintf("Buffer Size: %d KB", Settings.BufferSize))
fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout))) fmt.Println(fmt.Sprintf("Timeout: %d ms", int(Settings.BufferTimeout)))
fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent)) fmt.Println(fmt.Sprintf("User Agent: %s", Settings.UserAgent))

View File

@@ -99,6 +99,7 @@ type SystemStruct struct {
} }
URLBase string URLBase string
UDPxy string
Version string Version string
WEB struct { WEB struct {
Menu []string Menu []string
@@ -246,8 +247,8 @@ type Notification struct {
Type string `json:"type,required"` Type string `json:"type,required"`
} }
// SettingsStrcut : Inhalt der settings.json // SettingsStruct : Inhalt der settings.json
type SettingsStrcut struct { type SettingsStruct struct {
API bool `json:"api"` API bool `json:"api"`
AuthenticationAPI bool `json:"authentication.api"` AuthenticationAPI bool `json:"authentication.api"`
AuthenticationM3U bool `json:"authentication.m3u"` AuthenticationM3U bool `json:"authentication.m3u"`
@@ -290,6 +291,7 @@ type SettingsStrcut struct {
UpdateURL string `json:"update.url,omitempty"` UpdateURL string `json:"update.url,omitempty"`
UserAgent string `json:"user.agent"` UserAgent string `json:"user.agent"`
UUID string `json:"uuid"` UUID string `json:"uuid"`
UDPxy string `json:"udpxy"`
Version string `json:"version"` Version string `json:"version"`
XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"` XepgReplaceMissingImages bool `json:"xepg.replace.missing.images"`
XteveAutoUpdate bool `json:"xteveAutoUpdate"` XteveAutoUpdate bool `json:"xteveAutoUpdate"`

View File

@@ -37,6 +37,7 @@ type RequestStruct struct {
FilesUpdate *bool `json:"files.update,omitempty"` FilesUpdate *bool `json:"files.update,omitempty"`
TempPath *string `json:"temp.path,omitempty"` TempPath *string `json:"temp.path,omitempty"`
Tuner *int `json:"tuner,omitempty"` Tuner *int `json:"tuner,omitempty"`
UDPxy *string `json:"udpxy,omitempty"`
Update *[]string `json:"update,omitempty"` Update *[]string `json:"update,omitempty"`
UserAgent *string `json:"user.agent,omitempty"` UserAgent *string `json:"user.agent,omitempty"`
XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"` XepgReplaceMissingImages *bool `json:"xepg.replace.missing.images,omitempty"`
@@ -109,7 +110,7 @@ type ResponseStruct struct {
OpenLink string `json:"openLink,omitempty"` OpenLink string `json:"openLink,omitempty"`
OpenMenu string `json:"openMenu,omitempty"` OpenMenu string `json:"openMenu,omitempty"`
Reload bool `json:"reload,omitempty"` Reload bool `json:"reload,omitempty"`
Settings SettingsStrcut `json:"settings,required"` Settings SettingsStruct `json:"settings,required"`
Status bool `json:"status,required"` Status bool `json:"status,required"`
Token string `json:"token,omitempty"` Token string `json:"token,omitempty"`
Users map[string]interface{} `json:"users,omitempty"` Users map[string]interface{} `json:"users,omitempty"`

View File

@@ -90,7 +90,7 @@ func createSystemFiles() (err error) {
} }
// Einstellungen laden und default Werte setzen (xTeVe) // Einstellungen laden und default Werte setzen (xTeVe)
func loadSettings() (settings SettingsStrcut, err error) { func loadSettings() (settings SettingsStruct, err error) {
settingsMap, err := loadJSONFileToMap(System.File.Settings) settingsMap, err := loadJSONFileToMap(System.File.Settings)
if err != nil { if err != nil {
@@ -135,6 +135,7 @@ func loadSettings() (settings SettingsStrcut, err error) {
defaults["update"] = []string{"0000"} defaults["update"] = []string{"0000"}
defaults["user.agent"] = System.Name defaults["user.agent"] = System.Name
defaults["uuid"] = createUUID() defaults["uuid"] = createUUID()
defaults["udpxy"] = ""
defaults["version"] = System.DBVersion defaults["version"] = System.DBVersion
defaults["xteveAutoUpdate"] = true defaults["xteveAutoUpdate"] = true
defaults["temp.path"] = System.Folder.Temp defaults["temp.path"] = System.Folder.Temp
@@ -186,7 +187,7 @@ func loadSettings() (settings SettingsStrcut, err error) {
} }
// Einstellungen speichern (xTeVe) // Einstellungen speichern (xTeVe)
func saveSettings(settings SettingsStrcut) (err error) { func saveSettings(settings SettingsStruct) (err error) {
if settings.BackupKeep == 0 { if settings.BackupKeep == 0 {
settings.BackupKeep = 10 settings.BackupKeep = 10

File diff suppressed because one or more lines are too long

View File

@@ -30,6 +30,7 @@ func StartWebserver() (err error) {
http.HandleFunc("/api/", API) http.HandleFunc("/api/", API)
http.HandleFunc("/images/", Images) http.HandleFunc("/images/", Images)
http.HandleFunc("/data_images/", DataImages) http.HandleFunc("/data_images/", DataImages)
//http.HandleFunc("/auto/", Auto) //http.HandleFunc("/auto/", Auto)
showInfo("DVR IP:" + System.IPAddress + ":" + Settings.Port) showInfo("DVR IP:" + System.IPAddress + ":" + Settings.Port)
@@ -129,6 +130,12 @@ func Stream(w http.ResponseWriter, r *http.Request) {
return return
} }
// If an UDPxy host is set, and the stream URL is multicast (i.e. starts with 'udp://@'),
// then streamInfo.URL needs to be rewritten to point to UDPxy.
if Settings.UDPxy != "" && strings.HasPrefix(streamInfo.URL, "udp://@") {
streamInfo.URL = fmt.Sprintf("http://%s/udp/%s/", Settings.UDPxy, strings.TrimPrefix(streamInfo.URL, "udp://@"))
}
switch Settings.Buffer { switch Settings.Buffer {
case "-": case "-":

View File

@@ -22,7 +22,7 @@ menuItems.push(new MainMenuItem("logout", "{{.mainMenu.item.logout}}", "logout.p
// Kategorien für die Einstellungen // Kategorien für die Einstellungen
var settingsCategory = new Array() var settingsCategory = new Array()
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.general}}", "xteveAutoUpdate,tuner,epgSource,api"));settingsCategory.push(new SettingsCategoryItem("{{.settings.category.files}}", "update,files.update,temp.path,cache.images,xepg.replace.missing.images"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.streaming}}", "buffer,udpxy,buffer.size.kb,buffer.timeout,user.agent,ffmpeg.path,ffmpeg.options,vlc.path,vlc.options"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.backup}}", "backup.path,backup.keep"))
settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api")) settingsCategory.push(new SettingsCategoryItem("{{.settings.category.authentication}}", "authentication.web,authentication.pms,authentication.m3u,authentication.xml,authentication.api"))
@@ -408,7 +408,7 @@ function changeChannelNumber(element) {
}) })
for (var i = 0; i < channelNumbers.length; i++) { for (var i = 0; i < channelNumbers.length; i++) {
if (channelNumbers.indexOf(newNumber) == -1) { if (channelNumbers.indexOf(newNumber) == -1) {
break break
} }
@@ -422,7 +422,7 @@ function changeChannelNumber(element) {
} }
} }
data[dbID]["x-channelID"] = newNumber.toString() data[dbID]["x-channelID"] = newNumber.toString()
element.value = newNumber element.value = newNumber
@@ -461,7 +461,7 @@ function toggleChannelStatus(id:string) {
var checkbox = (document.getElementById("active") as HTMLInputElement) var checkbox = (document.getElementById("active") as HTMLInputElement)
status = (checkbox).checked status = (checkbox).checked
} }
var ids:string[] = getAllSelectedChannels() var ids:string[] = getAllSelectedChannels()
if (ids.length == 0) { if (ids.length == 0) {
@@ -482,9 +482,9 @@ function toggleChannelStatus(id:string) {
alert(channel["x-name"] + ": Missing XMLTV file / channel") alert(channel["x-name"] + ": Missing XMLTV file / channel")
checkbox.checked = false checkbox.checked = false
} }
channel["x-active"] = false channel["x-active"] = false
} }
break break
@@ -621,9 +621,9 @@ function checkUndo(key:string) {
UNDO[key] = JSON.parse(JSON.stringify(SERVER["xepg"][key])); UNDO[key] = JSON.parse(JSON.stringify(SERVER["xepg"][key]));
} }
break; break;
default: default:
break; break;
} }
@@ -646,9 +646,9 @@ function sortSelect(elem) {
elem.options[i] = tmpAry[i]; elem.options[i] = tmpAry[i];
if(elem.options[i].value == selectedValue) newSelectedIndex = i; if(elem.options[i].value == selectedValue) newSelectedIndex = i;
} }
elem.selectedIndex = newSelectedIndex; // Set new selected index after sorting elem.selectedIndex = newSelectedIndex; // Set new selected index after sorting
return; return;
} }

View File

@@ -372,19 +372,34 @@ class SettingsCategory {
setting.appendChild(tdRight) setting.appendChild(tdRight)
break break
case "udpxy":
var tdLeft = document.createElement("TD");
tdLeft.innerHTML = "{{.settings.udpxy.title}}" + ":"
var tdRight = document.createElement("TD")
var input = content.createInput("text", "udpxy", data)
input.setAttribute("placeholder", "{{.settings.udpxy.placeholder}}")
input.setAttribute("onchange", "javascript: this.className = 'changed'")
tdRight.appendChild(input)
setting.appendChild(tdLeft)
setting.appendChild(tdRight)
break
} }
return setting return setting
} }
createDescription(settingsKey:string):any { createDescription(settingsKey:string):any {
var description = document.createElement("TR") var description = document.createElement("TR")
var text:string var text:string
switch (settingsKey) { switch (settingsKey) {
case "authentication.web": case "authentication.web":
text = "{{.settings.authenticationWEB.description}}" text = "{{.settings.authenticationWEB.description}}"
break break
@@ -483,10 +498,14 @@ class SettingsCategory {
text = "{{.settings.replaceEmptyImages.description}}" text = "{{.settings.replaceEmptyImages.description}}"
break break
case "udpxy":
text = "{{.settings.udpxy.description}}"
break
default: default:
text = "" text = ""
break break
} }
var tdLeft = document.createElement("TD") var tdLeft = document.createElement("TD")
@@ -499,7 +518,7 @@ class SettingsCategory {
description.appendChild(tdLeft) description.appendChild(tdLeft)
description.appendChild(tdRight) description.appendChild(tdRight)
return description return description
} }
@@ -519,12 +538,12 @@ class SettingsCategoryItem extends SettingsCategory {
createCategory():void { createCategory():void {
var headline = this.createCategoryHeadline(this.headline) var headline = this.createCategoryHeadline(this.headline)
var settingsKeys = this.settingsKeys var settingsKeys = this.settingsKeys
var doc = document.getElementById(this.DocumentID) var doc = document.getElementById(this.DocumentID)
doc.appendChild(headline) doc.appendChild(headline)
// Tabelle für die Kategorie erstellen // Tabelle für die Kategorie erstellen
var table = document.createElement("TABLE") var table = document.createElement("TABLE")
var keys = settingsKeys.split(",") var keys = settingsKeys.split(",")
@@ -565,7 +584,7 @@ function showSettings() {
for (let i = 0; i < settingsCategory.length; i++) { for (let i = 0; i < settingsCategory.length; i++) {
settingsCategory[i].createCategory() settingsCategory[i].createCategory()
} }
} }
function saveSettings() { function saveSettings() {
@@ -627,7 +646,7 @@ function saveSettings() {
break break
} }
} }
var data = new Object() var data = new Object()