diff --git a/dlna/dlna.go b/dlna/dlna.go index de098b5..211a7a0 100644 --- a/dlna/dlna.go +++ b/dlna/dlna.go @@ -32,7 +32,7 @@ func BinaryInt(b bool) uint { // "DLNA.ORG_OP=" time-seek-range-supp bytes-range-header-supp func (cf ContentFeatures) String() (ret string) { // DLNA.ORG_PN=[a-zA-Z0-9_]* - params := make([]string, 0, 2) + params := make([]string, 0, 3) if cf.ProfileName != "" { params = append(params, "DLNA.ORG_PN="+cf.ProfileName) } @@ -41,6 +41,7 @@ func (cf ContentFeatures) String() (ret string) { BinaryInt(cf.SupportTimeSeek), BinaryInt(cf.SupportRange), BinaryInt(cf.Transcoded))) + params = append(params, "DLNA.ORG_FLAGS=01700000000000000000000000000000") return strings.Join(params, ";") } diff --git a/dlna/dlna_test.go b/dlna/dlna_test.go index d149536..33b53f7 100644 --- a/dlna/dlna_test.go +++ b/dlna/dlna_test.go @@ -9,7 +9,7 @@ func TestContentFeaturesString(t *testing.T) { Transcoded: true, SupportTimeSeek: true, }.String() - e := "DLNA.ORG_OP=10;DLNA.ORG_CI=1" + e := "DLNA.ORG_OP=10;DLNA.ORG_CI=1;DLNA.ORG_FLAGS=01700000000000000000000000000000" if e != a { t.Fatal(a) } diff --git a/dlna/dms/cd-service-desc.go b/dlna/dms/cd-service-desc.go index c171cc4..4bf16cd 100644 --- a/dlna/dms/cd-service-desc.go +++ b/dlna/dms/cd-service-desc.go @@ -2,450 +2,505 @@ package dms const contentDirectoryServiceDescription = ` - - 1 - 0 - - - - GetSearchCapabilities - - - SearchCaps - out - SearchCapabilities - - - - - GetSortCapabilities - - - SortCaps - out - SortCapabilities - - - - - GetSortExtensionCapabilities - - - SortExtensionCaps - out - SortExtensionCapabilities - - - - - GetFeatureList - - - FeatureList - out - FeatureList - - - - - GetSystemUpdateID - - - Id - out - SystemUpdateID - - - - - Browse - - - ObjectID - in - A_ARG_TYPE_ObjectID - - - BrowseFlag - in - A_ARG_TYPE_BrowseFlag - - - Filter - in - A_ARG_TYPE_Filter - - - StartingIndex - in - A_ARG_TYPE_Index - - - RequestedCount - in - A_ARG_TYPE_Count - - - SortCriteria - in - A_ARG_TYPE_SortCriteria - - - Result - out - A_ARG_TYPE_Result - - - NumberReturned - out - A_ARG_TYPE_Count - - - TotalMatches - out - A_ARG_TYPE_Count - - - UpdateID - out - A_ARG_TYPE_UpdateID - - - - - Search - - - ContainerID - in - A_ARG_TYPE_ObjectID - - - SearchCriteria - in - A_ARG_TYPE_SearchCriteria - - - Filter - in - A_ARG_TYPE_Filter - - - StartingIndex - in - A_ARG_TYPE_Index - - - RequestedCount - in - A_ARG_TYPE_Count - - - SortCriteria - in - A_ARG_TYPE_SortCriteria - - - Result - out - A_ARG_TYPE_Result - - - NumberReturned - out - A_ARG_TYPE_Count - - - TotalMatches - out - A_ARG_TYPE_Count - - - UpdateID - out - A_ARG_TYPE_UpdateID - - - - - CreateObject - - - ContainerID - in - A_ARG_TYPE_ObjectID - - - Elements - in - A_ARG_TYPE_Result - - - ObjectID - out - A_ARG_TYPE_ObjectID - - - Result - out - A_ARG_TYPE_Result - - - - - DestroyObject - - - ObjectID - in - A_ARG_TYPE_ObjectID - - - - - UpdateObject - - - ObjectID - in - A_ARG_TYPE_ObjectID - - - CurrentTagValue - in - A_ARG_TYPE_TagValueList - - - NewTagValue - in - A_ARG_TYPE_TagValueList - - - - - MoveObject - - - ObjectID - in - A_ARG_TYPE_ObjectID - - - NewParentID - in - A_ARG_TYPE_ObjectID - - - NewObjectID - out - A_ARG_TYPE_ObjectID - - - - - ImportResource - - - SourceURI - in - A_ARG_TYPE_URI - - - DestinationURI - in - A_ARG_TYPE_URI - - - TransferID - out - A_ARG_TYPE_TransferID - - - - - ExportResource - - - SourceURI - in - A_ARG_TYPE_URI - - - DestinationURI - in - A_ARG_TYPE_URI - - - TransferID - out - A_ARG_TYPE_TransferID - - - - - StopTransferResource - - - TransferID - in - A_ARG_TYPE_TransferID - - - - - DeleteResource - - - ResourceURI - in - A_ARG_TYPE_URI - - - - - GetTransferProgress - - - TransferID - in - A_ARG_TYPE_TransferID - - - TransferStatus - out - A_ARG_TYPE_TransferStatus - - - TransferLength - out - A_ARG_TYPE_TransferLength - - - TransferTotal - out - A_ARG_TYPE_TransferTotal - - - - - CreateReference - - - ContainerID - in - A_ARG_TYPE_ObjectID - - - ObjectID - in - A_ARG_TYPE_ObjectID - - - NewID - out - A_ARG_TYPE_ObjectID - - - - - - - SearchCapabilities - string - - - SortCapabilities - string - - - SortExtensionCapabilities - string - - - SystemUpdateID - ui4 - - - ContainerUpdateIDs - string - - - TransferIDs - string - - - FeatureList - string - - - A_ARG_TYPE_ObjectID - string - - - A_ARG_TYPE_Result - string - - - A_ARG_TYPE_SearchCriteria - string - - - A_ARG_TYPE_BrowseFlag - string - - BrowseMetadata - BrowseDirectChildren - - - - A_ARG_TYPE_Filter - string - - - A_ARG_TYPE_SortCriteria - string - - - A_ARG_TYPE_Index - ui4 - - - A_ARG_TYPE_Count - ui4 - - - A_ARG_TYPE_UpdateID - ui4 - - - A_ARG_TYPE_TransferID - ui4 - - - A_ARG_TYPE_TransferStatus - string - - COMPLETED - ERROR - IN_PROGRESS - STOPPED - - - - A_ARG_TYPE_TransferLength - string - - - A_ARG_TYPE_TransferTotal - string - - - A_ARG_TYPE_TagValueList - string - - - A_ARG_TYPE_URI - uri - - + + 1 + 0 + + + + GetSearchCapabilities + + + SearchCaps + out + SearchCapabilities + + + + + GetSortCapabilities + + + SortCaps + out + SortCapabilities + + + + + GetSortExtensionCapabilities + + + SortExtensionCaps + out + SortExtensionCapabilities + + + + + GetFeatureList + + + FeatureList + out + FeatureList + + + + + GetSystemUpdateID + + + Id + out + SystemUpdateID + + + + + Browse + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + BrowseFlag + in + A_ARG_TYPE_BrowseFlag + + + Filter + in + A_ARG_TYPE_Filter + + + StartingIndex + in + A_ARG_TYPE_Index + + + RequestedCount + in + A_ARG_TYPE_Count + + + SortCriteria + in + A_ARG_TYPE_SortCriteria + + + Result + out + A_ARG_TYPE_Result + + + NumberReturned + out + A_ARG_TYPE_Count + + + TotalMatches + out + A_ARG_TYPE_Count + + + UpdateID + out + A_ARG_TYPE_UpdateID + + + + + Search + + + ContainerID + in + A_ARG_TYPE_ObjectID + + + SearchCriteria + in + A_ARG_TYPE_SearchCriteria + + + Filter + in + A_ARG_TYPE_Filter + + + StartingIndex + in + A_ARG_TYPE_Index + + + RequestedCount + in + A_ARG_TYPE_Count + + + SortCriteria + in + A_ARG_TYPE_SortCriteria + + + Result + out + A_ARG_TYPE_Result + + + NumberReturned + out + A_ARG_TYPE_Count + + + TotalMatches + out + A_ARG_TYPE_Count + + + UpdateID + out + A_ARG_TYPE_UpdateID + + + + + CreateObject + + + ContainerID + in + A_ARG_TYPE_ObjectID + + + Elements + in + A_ARG_TYPE_Result + + + ObjectID + out + A_ARG_TYPE_ObjectID + + + Result + out + A_ARG_TYPE_Result + + + + + DestroyObject + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + + + UpdateObject + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + CurrentTagValue + in + A_ARG_TYPE_TagValueList + + + NewTagValue + in + A_ARG_TYPE_TagValueList + + + + + MoveObject + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + NewParentID + in + A_ARG_TYPE_ObjectID + + + NewObjectID + out + A_ARG_TYPE_ObjectID + + + + + ImportResource + + + SourceURI + in + A_ARG_TYPE_URI + + + DestinationURI + in + A_ARG_TYPE_URI + + + TransferID + out + A_ARG_TYPE_TransferID + + + + + ExportResource + + + SourceURI + in + A_ARG_TYPE_URI + + + DestinationURI + in + A_ARG_TYPE_URI + + + TransferID + out + A_ARG_TYPE_TransferID + + + + + StopTransferResource + + + TransferID + in + A_ARG_TYPE_TransferID + + + + + DeleteResource + + + ResourceURI + in + A_ARG_TYPE_URI + + + + + GetTransferProgress + + + TransferID + in + A_ARG_TYPE_TransferID + + + TransferStatus + out + A_ARG_TYPE_TransferStatus + + + TransferLength + out + A_ARG_TYPE_TransferLength + + + TransferTotal + out + A_ARG_TYPE_TransferTotal + + + + + CreateReference + + + ContainerID + in + A_ARG_TYPE_ObjectID + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + NewID + out + A_ARG_TYPE_ObjectID + + + + + X_GetFeatureList + + + FeatureList + out + A_ARG_TYPE_Featurelist + + + + + X_SetBookmark + + + CategoryType + in + A_ARG_TYPE_CategoryType + + + RID + in + A_ARG_TYPE_RID + + + ObjectID + in + A_ARG_TYPE_ObjectID + + + PosSecond + in + A_ARG_TYPE_PosSec + + + + + + + SearchCapabilities + string + + + SortCapabilities + string + + + SortExtensionCapabilities + string + + + SystemUpdateID + ui4 + + + ContainerUpdateIDs + string + + + TransferIDs + string + + + FeatureList + string + + + A_ARG_TYPE_ObjectID + string + + + A_ARG_TYPE_Result + string + + + A_ARG_TYPE_SearchCriteria + string + + + A_ARG_TYPE_BrowseFlag + string + + BrowseMetadata + BrowseDirectChildren + + + + A_ARG_TYPE_Filter + string + + + A_ARG_TYPE_SortCriteria + string + + + A_ARG_TYPE_Index + ui4 + + + A_ARG_TYPE_Count + ui4 + + + A_ARG_TYPE_UpdateID + ui4 + + + A_ARG_TYPE_TransferID + ui4 + + + A_ARG_TYPE_TransferStatus + string + + COMPLETED + ERROR + IN_PROGRESS + STOPPED + + + + A_ARG_TYPE_TransferLength + string + + + A_ARG_TYPE_TransferTotal + string + + + A_ARG_TYPE_TagValueList + string + + + A_ARG_TYPE_URI + uri + + + A_ARG_TYPE_CategoryType + ui4 + + + + A_ARG_TYPE_RID + ui4 + + + + A_ARG_TYPE_PosSec + ui4 + + + + A_ARG_TYPE_Featurelist + string + + + ` diff --git a/dlna/dms/cds.go b/dlna/dms/cds.go index b394c85..b408d3f 100644 --- a/dlna/dms/cds.go +++ b/dlna/dms/cds.go @@ -287,6 +287,21 @@ func (me *contentDirectoryService) Handle(action string, argsXML []byte, r *http return [][2]string{ {"SearchCaps", ""}, }, nil + // Samsung Extensions + case "X_GetFeatureList": + // TODO: make it dependable on model + // https://github.com/1100101/minidlna/blob/ca6dbba18390ad6f8b8d7b7dbcf797dbfd95e2db/upnpsoap.c#L2153-L2199 + return [][2]string{ + {"FeatureList", ` + + // "A" + // "V" + // "I" + +`}}, nil + case "X_SetBookmark": + // just ignore + return [][2]string{}, nil default: return nil, upnp.InvalidActionError } diff --git a/dlna/dms/cm-service-desc.go b/dlna/dms/cm-service-desc.go index 855d19c..f9cf9ae 100644 --- a/dlna/dms/cm-service-desc.go +++ b/dlna/dms/cm-service-desc.go @@ -1,275 +1,185 @@ package dms -const connectionManagerServiceDesc = ` +const connectionManagerServiceDescription = ` - - 1 - 0 - - - - GetProtocolInfo - - - Source - out - -SourceProtocolInfo - - - - Sink - out - -SinkProtocolInfo - - - - - - PrepareForConnection - - - RemoteProtocolInfo - in - -A_ARG_TYPE_ProtocolInfo - - - - PeerConnectionManager - in - -A_ARG_TYPE_ConnectionManager - - - - PeerConnectionID - in - -A_ARG_TYPE_ConnectionID - - - - Direction - in - -A_ARG_TYPE_Direction - - - - ConnectionID - out - -A_ARG_TYPE_ConnectionID - - - - AVTransportID - out - A_ARG_TYPE_AVTransportID - - - RcsID - out - A_ARG_TYPE_RcsID - - - - - ConnectionComplete - - - ConnectionID - in - A_ARG_TYPE_ConnectionID - - - - - GetCurrentConnectionIDs - - - ConnectionIDs - out - -CurrentConnectionIDs - - - - - - GetCurrentConnectionInfo - - - ConnectionID - in - -A_ARG_TYPE_ConnectionID - - - - RcsID - out - -A_ARG_TYPE_RcsID - - - - AVTransportID - out - -A_ARG_TYPE_AVTransportID - - - - ProtocolInfo - out - -A_ARG_TYPE_ProtocolInfo - - - - PeerConnectionManager - out - -A_ARG_TYPE_ConnectionManager - - - - PeerConnectionID - out - -A_ARG_TYPE_ConnectionID - - - - Direction - out - -A_ARG_TYPE_Direction - - - - Status - out - -A_ARG_TYPE_ConnectionStatus - - - - - - GetRendererItemInfo - - - ItemInfoFilter - in - -A_ARG_TYPE_ItemInfoFilter - - - - ItemMetadataList - in - -A_ARG_TYPE_Result - - - - ItemRenderingInfoList - out - -A_ARG_TYPE_RenderingInfoList - - - - - - GetFeatureList - - - FeatureList - out - -FeatureList - - - - - - - - SourceProtocolInfo - string - - - SinkProtocolInfo - string - - - CurrentConnectionIDs - string - - - A_ARG_TYPE_ConnectionStatus - string - - OK - ContentFormatMismatch - InsufficientBandwidth - UnreliableChannel - Unknown - - - - A_ARG_TYPE_ConnectionManager - string - - - A_ARG_TYPE_Direction - string - - Input - Output - - - - A_ARG_TYPE_ProtocolInfo - string - - - A_ARG_TYPE_ConnectionID - i4 - - - A_ARG_TYPE_AVTransportID - i4 - - - A_ARG_TYPE_RcsID - i4 - - - A_ARG_TYPE_ItemInfoFilter - string - - - A_ARG_TYPE_Result - string - - - A_ARG_TYPE_RenderingInfoList - string - - - ClockUpdateID - ui4 - - - DeviceClockInfoUpdates - string - - + + 1 + 0 + + + + GetProtocolInfo + + + Source + out + SourceProtocolInfo + + + Sink + out + SinkProtocolInfo + + + + + PrepareForConnection + + + RemoteProtocolInfo + in + A_ARG_TYPE_ProtocolInfo + + + PeerConnectionManager + in + A_ARG_TYPE_ConnectionManager + + + PeerConnectionID + in + A_ARG_TYPE_ConnectionID + + + Direction + in + A_ARG_TYPE_Direction + + + ConnectionID + out + A_ARG_TYPE_ConnectionID + + + AVTransportID + out + A_ARG_TYPE_AVTransportID + + + RcsID + out + A_ARG_TYPE_RcsID + + + + + ConnectionComplete + + + ConnectionID + in + A_ARG_TYPE_ConnectionID + + + + + GetCurrentConnectionIDs + + + ConnectionIDs + out + CurrentConnectionIDs + + + + + GetCurrentConnectionInfo + + + ConnectionID + in + A_ARG_TYPE_ConnectionID + + + RcsID + out + A_ARG_TYPE_RcsID + + + AVTransportID + out + A_ARG_TYPE_AVTransportID + + + ProtocolInfo + out + A_ARG_TYPE_ProtocolInfo + + + PeerConnectionManager + out + A_ARG_TYPE_ConnectionManager + + + PeerConnectionID + out + A_ARG_TYPE_ConnectionID + + + Direction + out + A_ARG_TYPE_Direction + + + Status + out + A_ARG_TYPE_ConnectionStatus + + + + + + + SourceProtocolInfo + string + + + SinkProtocolInfo + string + + + CurrentConnectionIDs + string + + + A_ARG_TYPE_ConnectionStatus + string + + OK + ContentFormatMismatch + InsufficientBandwidth + UnreliableChannel + Unknown + + + + A_ARG_TYPE_ConnectionManager + string + + + A_ARG_TYPE_Direction + string + + Input + Output + + + + A_ARG_TYPE_ProtocolInfo + string + + + A_ARG_TYPE_ConnectionID + i4 + + + A_ARG_TYPE_AVTransportID + i4 + + + A_ARG_TYPE_RcsID + i4 + + ` diff --git a/dlna/dms/cms.go b/dlna/dms/cms.go new file mode 100644 index 0000000..caacc6f --- /dev/null +++ b/dlna/dms/cms.go @@ -0,0 +1,42 @@ +package dms + +import ( + "net/http" + + "github.com/anacrolix/dms/upnp" +) + +//const defaultProtocolInfo = "http-get:*:video/mpeg:*,http-get:*:video/mp4:*,http-get:*:video/vnd.dlna.mpeg-tts:*,http-get:*:video/avi:*,http-get:*:video/x-matroska:*,http-get:*:video/x-ms-wmv:*,http-get:*:video/wtv:*,http-get:*:audio/mpeg:*,http-get:*:audio/mp3:*,http-get:*:audio/mp4:*,http-get:*:audio/x-ms-wma*,http-get:*:audio/wav:*,http-get:*:audio/L16:*,http-get:*image/jpeg:*,http-get:*image/png:*,http-get:*image/gif:*,http-get:*image/tiff:*" +const defaultProtocolInfo = "http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_TN,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_SM,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_MED,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_LRG,http-get:*:image/jpeg:DLNA.ORG_PN=JPEG_RES_H_V,http-get:*:image/png:DLNA.ORG_PN=PNG_TN,http-get:*:image/png:DLNA.ORG_PN=PNG_LRG,http-get:*:image/gif:DLNA.ORG_PN=GIF_LRG,http-get:*:audio/mpeg:DLNA.ORG_PN=MP3,http-get:*:audio/L16:DLNA.ORG_PN=LPCM,http-get:*:video/mpeg:DLNA.ORG_PN=AVC_TS_HD_24_AC3_ISO;SONY.COM_PN=AVC_TS_HD_24_AC3_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_24_AC3;SONY.COM_PN=AVC_TS_HD_24_AC3,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_24_AC3_T;SONY.COM_PN=AVC_TS_HD_24_AC3_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_PS_PAL,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_PS_NTSC,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_50_L2_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_60_L2_T,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_SD_EU_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_EU_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_50_AC3_T,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_HD_50_L2_ISO;SONY.COM_PN=HD2_50_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_SD_60_AC3_T,http-get:*:video/mpeg:DLNA.ORG_PN=MPEG_TS_HD_60_L2_ISO;SONY.COM_PN=HD2_60_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_HD_50_L2_T;SONY.COM_PN=HD2_50_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=MPEG_TS_HD_60_L2_T;SONY.COM_PN=HD2_60_T,http-get:*:video/mpeg:DLNA.ORG_PN=AVC_TS_HD_50_AC3_ISO;SONY.COM_PN=AVC_TS_HD_50_AC3_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_50_AC3;SONY.COM_PN=AVC_TS_HD_50_AC3,http-get:*:video/mpeg:DLNA.ORG_PN=AVC_TS_HD_60_AC3_ISO;SONY.COM_PN=AVC_TS_HD_60_AC3_ISO,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_60_AC3;SONY.COM_PN=AVC_TS_HD_60_AC3,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_50_AC3_T;SONY.COM_PN=AVC_TS_HD_50_AC3_T,http-get:*:video/vnd.dlna.mpeg-tts:DLNA.ORG_PN=AVC_TS_HD_60_AC3_T;SONY.COM_PN=AVC_TS_HD_60_AC3_T,http-get:*:video/x-mp2t-mphl-188:*,http-get:*:video/*:*,http-get:*:audio/*:*,http-get:*:image/*:*,http-get:*:text/srt:*,http-get:*:text/smi:*,http-get:*:text/ssa:*,http-get:*:*:*" + +type connectionManagerService struct { + *Server + upnp.Eventing +} + +func (cms *connectionManagerService) Handle(action string, argsXML []byte, r *http.Request) ([][2]string, error) { + switch action { + case ".GetCurrentConnectionInfo": + return [][2]string{ + {"ConnectionID", "0"}, + {"RcsID", "-1"}, + {"AVTransportID", "-1"}, + {"ProtocolInfo", ""}, + {"PeerConnectionManager", ""}, + {"PeerConnectionID", "-1"}, + {"Direction", "Output"}, + {"Status", "OK"}, + }, nil + case "GetCurrentConnectionIDs": + return [][2]string{ + {"ConnectionIDs", ""}, + }, nil + case "GetProtocolInfo": + return [][2]string{ + {"Source", defaultProtocolInfo}, + {"Sink", ""}, + }, nil + default: + return nil, upnp.InvalidActionError + } +} diff --git a/dlna/dms/dms.go b/dlna/dms/dms.go index c4d7850..e2bc012 100644 --- a/dlna/dms/dms.go +++ b/dlna/dms/dms.go @@ -18,6 +18,7 @@ import ( "os/user" "path" "path/filepath" + "strconv" "strings" "time" @@ -37,7 +38,6 @@ const ( resPath = "/res" iconPath = "/icon" rootDescPath = "/rootDesc.xml" - contentDirectorySCPDURL = "/scpd/ContentDirectory.xml" contentDirectoryEventSubURL = "/evt/ContentDirectory" serviceControlURL = "/ctl" deviceIconPath = "/deviceIcon" @@ -85,13 +85,20 @@ var services = []*service{ }, SCPD: contentDirectoryServiceDescription, }, - // { - // Service: upnp.Service{ - // ServiceType: "urn:schemas-upnp-org:service:ConnectionManager:3", - // ServiceId: "urn:upnp-org:serviceId:ConnectionManager", - // }, - // SCPD: connectionManagerServiceDesc, - // }, + { + Service: upnp.Service{ + ServiceType: "urn:schemas-upnp-org:service:ConnectionManager:1", + ServiceId: "urn:upnp-org:serviceId:ConnectionManager", + }, + SCPD: connectionManagerServiceDescription, + }, + { + Service: upnp.Service{ + ServiceType: "urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1", + ServiceId: "urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar", + }, + SCPD: mediaReceiverRegistrarDescription, + }, } // The control URL for every service is the same. We're able to infer the desired service from the request headers. @@ -487,8 +494,9 @@ func (me *mitmRespWriter) CloseNotify() <-chan bool { // Set the SCPD serve paths. func init() { for _, s := range services { - p := path.Join("/scpd", s.ServiceId) - s.SCPDURL = p + lastInd := strings.LastIndex(s.ServiceId, ":") + p := path.Join("/scpd", s.ServiceId[lastInd+1:]) + s.SCPDURL = p + ".xml" } } @@ -498,7 +506,7 @@ func handleSCPDs(mux *http.ServeMux) { mux.HandleFunc(s.SCPDURL, func(serviceDesc string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", `text/xml; charset="utf-8"`) - http.ServeContent(w, r, ".xml", startTime, bytes.NewReader([]byte(serviceDesc))) + http.ServeContent(w, r, "", startTime, bytes.NewReader([]byte(serviceDesc))) } }(s.SCPD)) } @@ -594,7 +602,10 @@ func (me *Server) serveIcon(w http.ResponseWriter, r *http.Request) { // cmd.Stderr = os.Stderr body, err := cmd.Output() if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + // serve 1st Icon if no ffmpegthumbnailer + w.Header().Set("Content-Type", me.Icons[0].Mimetype) + http.ServeContent(w, r, "", time.Time{}, me.Icons[0].ReadSeeker) + //http.Error(w, err.Error(), http.StatusInternalServerError) return } http.ServeContent(w, r, "", time.Now(), bytes.NewReader(body)) @@ -771,11 +782,19 @@ func (server *Server) initMux(mux *http.ServeMux) { handleSCPDs(mux) mux.HandleFunc(serviceControlURL, server.serviceControlHandler) mux.HandleFunc("/debug/pprof/", pprof.Index) - for i, di := range server.Icons { - mux.HandleFunc(fmt.Sprintf("%s/%d", deviceIconPath, i), func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", di.Mimetype) - http.ServeContent(w, r, "", time.Time{}, di.ReadSeeker) - }) + // DeviceIcons + iconHandl := func(w http.ResponseWriter, r *http.Request) { + idStr := path.Base(r.URL.Path) + id, _ := strconv.Atoi(idStr) + if id < 0 || id >= len(server.Icons) { + id = 0 + } + di := server.Icons[id] + w.Header().Set("Content-Type", di.Mimetype) + http.ServeContent(w, r, "", time.Time{}, di.ReadSeeker) + } + for i, _ := range server.Icons { + mux.HandleFunc(fmt.Sprintf("%s/%d", deviceIconPath, i), iconHandl) } } @@ -784,10 +803,24 @@ func (s *Server) initServices() (err error) { if err != nil { return } + urn1, err := upnp.ParseServiceType(services[1].ServiceType) + if err != nil { + return + } + urn2, err := upnp.ParseServiceType(services[2].ServiceType) + if err != nil { + return + } s.services = map[string]UPnPService{ urn.Type: &contentDirectoryService{ Server: s, }, + urn1.Type: &connectionManagerService{ + Server: s, + }, + urn2.Type: &mediaReceiverRegistrarService{ + Server: s, + }, } return } @@ -827,6 +860,8 @@ func (srv *Server) Init() (err error) { srv.rootDeviceUUID = makeDeviceUuid(srv.FriendlyName) srv.rootDescXML, err = xml.MarshalIndent( upnp.DeviceDesc{ + NSDLNA: "urn:schemas-dlna-org:device-1-0", + NSSEC: "http://www.sec.co.kr/dlna", SpecVersion: upnp.SpecVersion{Major: 1, Minor: 0}, Device: upnp.Device{ DeviceType: rootDeviceType, @@ -834,6 +869,12 @@ func (srv *Server) Init() (err error) { Manufacturer: "Matt Joiner ", ModelName: rootDeviceModelName, UDN: srv.rootDeviceUUID, + VendorXML: ` + + DMS-1.50 + M-DMS-1.50 + smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec + smi,DCM10,getMediaInfo.sec,getCaptionInfo.sec`, ServiceList: func() (ss []upnp.Service) { for _, s := range services { ss = append(ss, s.Service) @@ -852,6 +893,7 @@ func (srv *Server) Init() (err error) { } return }(), + PresentationURL: "/", }, }, " ", " ") diff --git a/dlna/dms/mrrs-desc.go b/dlna/dms/mrrs-desc.go new file mode 100644 index 0000000..07fb0a9 --- /dev/null +++ b/dlna/dms/mrrs-desc.go @@ -0,0 +1,90 @@ +package dms + +const mediaReceiverRegistrarDescription = ` + + + 1 + 0 + + + + IsAuthorized + + + DeviceID + in + A_ARG_TYPE_DeviceID + + + Result + out + A_ARG_TYPE_Result + + + + + RegisterDevice + + + RegistrationReqMsg + in + A_ARG_TYPE_RegistrationReqMsg + + + RegistrationRespMsg + out + A_ARG_TYPE_RegistrationRespMsg + + + + + IsValidated + + + DeviceID + in + A_ARG_TYPE_DeviceID + + + Result + out + A_ARG_TYPE_Result + + + + + + + A_ARG_TYPE_DeviceID + string + + + A_ARG_TYPE_Result + int + + + A_ARG_TYPE_RegistrationReqMsg + bin.base64 + + + A_ARG_TYPE_RegistrationRespMsg + bin.base64 + + + AuthorizationGrantedUpdateID + ui4 + + + AuthorizationDeniedUpdateID + ui4 + + + ValidationSucceededUpdateID + ui4 + + + ValidationRevokedUpdateID + ui4 + + +` diff --git a/dlna/dms/mrrs.go b/dlna/dms/mrrs.go new file mode 100644 index 0000000..08c8592 --- /dev/null +++ b/dlna/dms/mrrs.go @@ -0,0 +1,28 @@ +package dms + +import ( + "net/http" + + "github.com/anacrolix/dms/upnp" +) + +type mediaReceiverRegistrarService struct { + *Server + upnp.Eventing +} + +func (mrrs *mediaReceiverRegistrarService) Handle(action string, argsXML []byte, r *http.Request) ([][2]string, error) { + switch action { + case "IsAuthorized", "IsValidated": + return [][2]string{ + {"Result", "1"}, + }, nil + case "RegisterDevice": + return [][2]string{ + {"RegistrationRespMsg", mrrs.rootDeviceUUID}, + }, nil + // return nil, nil + default: + return nil, upnp.InvalidActionError + } +} diff --git a/ssdp/ssdp.go b/ssdp/ssdp.go index b099ad0..b622aa8 100644 --- a/ssdp/ssdp.go +++ b/ssdp/ssdp.go @@ -106,7 +106,13 @@ func makeConn(ifi net.Interface) (ret *net.UDPConn, err error) { func (me *Server) serve() { for { - b := make([]byte, me.Interface.MTU) + size := me.Interface.MTU + if size > 65536 { + size = 65536 + } else if size <= 0 { // fix for windows with mtu 4gb + size = 65536 + } + b := make([]byte, size) n, addr, err := me.conn.ReadFromUDP(b) select { case <-me.closed: diff --git a/upnp/upnp.go b/upnp/upnp.go index eef9295..beea9e7 100644 --- a/upnp/upnp.go +++ b/upnp/upnp.go @@ -10,15 +10,16 @@ import ( "strings" ) -var serviceURNRegexp *regexp.Regexp = regexp.MustCompile(`^urn:schemas-upnp-org:service:(\w+):(\d+)$`) +var serviceURNRegexp *regexp.Regexp = regexp.MustCompile(`^urn:(.*):service:(\w+):(\d+)$`) type ServiceURN struct { + Auth string Type string Version uint64 } func (me ServiceURN) String() string { - return fmt.Sprintf("urn:schemas-upnp-org:service:%s:%d", me.Type, me.Version) + return fmt.Sprintf("urn:%s:service:%s:%d", me.Auth, me.Type, me.Version) } func ParseServiceType(s string) (ret ServiceURN, err error) { @@ -27,11 +28,12 @@ func ParseServiceType(s string) (ret ServiceURN, err error) { err = errors.New(s) return } - if len(matches) != 3 { - log.Panicf("Invalid serviceURNRegexp ?") + if len(matches) != 4 { + log.Panicf("Invalid serviceURNRegexp?") } - ret.Type = matches[1] - ret.Version, err = strconv.ParseUint(matches[2], 0, 0) + ret.Auth = matches[1] + ret.Type = matches[2] + ret.Version, err = strconv.ParseUint(matches[3], 0, 0) return } @@ -80,17 +82,21 @@ type Service struct { } type Device struct { - DeviceType string `xml:"deviceType"` - FriendlyName string `xml:"friendlyName"` - Manufacturer string `xml:"manufacturer"` - ModelName string `xml:"modelName"` - UDN string - IconList []Icon `xml:"iconList>icon"` - ServiceList []Service `xml:"serviceList>service"` + DeviceType string `xml:"deviceType"` + FriendlyName string `xml:"friendlyName"` + Manufacturer string `xml:"manufacturer"` + ModelName string `xml:"modelName"` + UDN string + VendorXML string `xml:",innerxml"` + IconList []Icon `xml:"iconList>icon"` + ServiceList []Service `xml:"serviceList>service"` + PresentationURL string `xml:"presentationURL,omitempty"` } type DeviceDesc struct { XMLName xml.Name `xml:"urn:schemas-upnp-org:device-1-0 root"` + NSDLNA string `xml:"xmlns:dlna,attr"` + NSSEC string `xml:"xmlns:sec,attr"` SpecVersion SpecVersion `xml:"specVersion"` Device Device `xml:"device"` } diff --git a/upnpav/upnpav.go b/upnpav/upnpav.go index b57b359..b19b686 100644 --- a/upnpav/upnpav.go +++ b/upnpav/upnpav.go @@ -2,12 +2,15 @@ package upnpav import ( "encoding/xml" + "time" ) const ( + // NoSuchObjectErrorCode : The specified ObjectID is invalid. NoSuchObjectErrorCode = 701 ) +// Resource description type Resource struct { XMLName xml.Name `xml:"res"` ProtocolInfo string `xml:"protocolInfo,attr"` @@ -18,28 +21,44 @@ type Resource struct { Resolution string `xml:"resolution,attr,omitempty"` } +// Container description type Container struct { Object XMLName xml.Name `xml:"container"` ChildCount int `xml:"childCount,attr"` } +// Item description type Item struct { Object - XMLName xml.Name `xml:"item"` - Res []Resource + XMLName xml.Name `xml:"item"` + Res []Resource + InnerXML string `xml:",innerxml"` } +// Object description type Object struct { - ID string `xml:"id,attr"` - ParentID string `xml:"parentID,attr"` - Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable - Title string `xml:"dc:title"` - Class string `xml:"upnp:class"` - Icon string `xml:"upnp:icon,omitempty"` - Artist string `xml:"upnp:artist,omitempty"` - Album string `xml:"upnp:album,omitempty"` - Genre string `xml:"upnp:genre,omitempty"` - AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` - Searchable int `xml:"searchable,attr"` + ID string `xml:"id,attr"` + ParentID string `xml:"parentID,attr"` + Restricted int `xml:"restricted,attr"` // indicates whether the object is modifiable + Title string `xml:"dc:title"` + Class string `xml:"upnp:class"` + Icon string `xml:"upnp:icon,omitempty"` + Date Timestamp `xml:"dc:date"` + Artist string `xml:"upnp:artist,omitempty"` + Album string `xml:"upnp:album,omitempty"` + Genre string `xml:"upnp:genre,omitempty"` + AlbumArtURI string `xml:"upnp:albumArtURI,omitempty"` + Searchable int `xml:"searchable,attr"` + SearchXML string `xml:",innerxml"` +} + +// Timestamp wraps time.Time for formatting purposes +type Timestamp struct { + time.Time +} + +// MarshalXML formats the Timestamp per DIDL-Lite spec +func (t Timestamp) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + return e.EncodeElement(t.Format("2006-01-02"), start) }