MusicBee UPnP Plugin 도움말

새로운 기능

2.0.2 - 2026-07-20

  • 템플릿을 따르는 노드가 있는 동안에는 템플릿을 더 이상 삭제할 수 없습니다. 삭제 버튼은 비활성화된 상태로 유지되므로 노드가 고아가 되는 일이 없습니다. 이제 각 템플릿 행에는 팔로워의 실시간 (n) 수가 표시되어 비활성화된 삭제를 한눈에 설명합니다. 사용하지 않는 템플릿을 삭제하면 다음 로드 시 기본값이 조용히 다시 나타나는 대신 이제 유지됩니다.
  • 보기 트리 옆에 있는 새로운 깔때기 버튼은 어떤 노드가 템플릿을 따르는지 정확히 보여줍니다. 선택한 템플릿의 팔로워만 표시하도록 트리를 필터링하고, 다른 템플릿을 선택하면 다시 필터링하며, 토글을 끄면 전체 트리를 복원합니다.
  • 라디오팟캐스트 노드는 이제 해당 카테고리의 템플릿과 영구적으로 페어링됩니다. 경로 탭에서 템플릿을 재구성하면 노드가 자동으로 따라갑니다. 적용할 것이 없으므로 적용 버튼은 비활성화됩니다. 템플릿 목록은 표준(모든 새 템플릿이 생성되는 곳) 및 예약됨(라디오 + 팟캐스트)의 두 밴드로 이를 반영합니다.

2.0.1 - 2026-07-20

  • 이제 경로 템플릿이 해당 템플릿을 사용하는 노드에 라이브 링크됩니다. 템플릿을 적용하면 노드가 템플릿을 따릅니다. 나중에 템플릿을 편집하면 해당 템플릿을 따르는 모든 노드가 즉시 재구성됩니다. 노드를 일일이 찾아 다시 적용할 필요가 없습니다. 보기 트리에는 각 노드가 이름 바로 뒤에 따르는 템플릿이 표시되고, 이름 변경이 즉시 나타나며, 템플릿을 삭제하면 먼저 해당 템플릿을 따르는 노드 수를 알려줍니다(노드는 현재 레이아웃을 유지하고 아무것도 따르지 않게 됩니다).
  • 숨겨진 노드에 템플릿을 적용하면 다시 표시됩니다. 적용은 "이것을 저렇게 보여줘" 제스처인 반면, 숨기기는 표시 확인란에 남아 있습니다. 이 기능을 대체하는 예약된 "숨김" 템플릿은 사라졌습니다.
  • 보기 트리는 더 이상 위치를 잃지 않습니다. 틱, 확장된 폴더 및 스크롤 위치는 모두 템플릿 적용 및 기타 새로 고침 후에도 유지됩니다.

2.0.0 - 2026-06-16

이것은 MusicBee UPnP 플러그인의 오픈 소스 yaiol 포크의 첫 번째 공개 릴리스입니다. 이 포크에 새로 추가된 모든 내용과 원본 플러그인에 대한 수정 및 개선 사항의 두 부분으로 구성됩니다. 각 항목은 프로젝트의 내부 기능 카탈로그의 무엇 / 왜 형식을 유지하므로 모든 변경 사항의 근거가 변경 사항뿐만 아니라 페이지에 있습니다.

이 포크의 새로운 기능

MediaRenderer - MusicBee로 재생

N01 - MusicBee를 재생 대상으로 하는 렌더러

무엇: 일반적으로 이 플러그인은 한 방향으로 작동합니다. 휴대폰 또는 다른 장치가 MusicBee 라이브러리를 탐색하고 자체적으로 음악을 재생합니다. 이 기능은 반대 방향을 추가합니다. MusicBee가 플레이어가 되도록 합니다. 휴대폰의 컨트롤러 앱(예: BubbleUPnP)에서 데스크톱 MusicBee를 재생할 항목으로 선택한 다음 손으로 제어할 수 있습니다. 재생, 일시 중지, 중지, 앞으로 또는 뒤로 건너뛰기, 트랙의 특정 지점으로 이동, 볼륨 변경 또는 음소거.

왜: 휴대폰을 PC에 이미 있는 음악의 리모컨으로 바꿉니다. 소파에 앉아 휴대폰으로 라이브러리를 탐색하고 트랙을 탭하면 데스크톱에 연결된 스피커에서 음악이 나옵니다. 앉은 자리에서 완벽하게 제어할 수 있습니다. 원본 플러그인은 이 기능을 작동하는 기능으로 제공하지 않았습니다.

켜기: 기본적으로 꺼져 있습니다. 켜면 홈 네트워크의 모든 장치가 PC에서 재생을 시작할 수 있기 때문입니다. 설정 대화 상자의 일반 탭에서 확인란을 선택하여 활성화합니다. 플러그인의 세 가지 역할은 각각 자체 확인란을 가지고 있습니다. 내 라이브러리 공유 (서버), 다른 사람이 나에게 재생하도록 허용 (렌더러), 다른 장치로 재생 (제어 지점). 대화 상자는 활성화한 역할에 실제로 필요한 설정 탭만 표시하므로 자신에게 적용되지 않는 옵션에 직면할 일이 없습니다.

기기 구분: 렌더러에 원하는 이름을 지정할 수 있습니다(기본값은 "MusicBee (yaiol)"). 이 이름은 휴대폰의 재생 대상 목록에 나타나므로 두 대 이상의 PC에서 MusicBee를 실행할 때 어떤 PC가 어떤 PC인지 알 수 있습니다. 이름 변경은 다시 시작할 필요 없이 즉시 적용됩니다.

자체 재생 시 최상의 사운드: 휴대폰에서 MusicBee 자체 라이브러리를 탐색하고 해당 동일한 MusicBee로 트랙을 다시 보내면 플러그인은 자체 파일 중 하나를 재생하도록 요청받았음을 인식하고 디스크에서 직접 재생합니다. 결과는 정확하고 즉각적입니다. 비트 완벽하며 MusicBee 자체 이퀄라이저 및 볼륨 레벨링이 적용됩니다. 오디오를 네트워크로 불필요하게 밀어내고 다시 자체로 되돌리는 대신.

비공개로 실행: 세 가지 역할은 독립적으로 작동하므로 라이브러리 공유를 끄고 렌더러를 켤 수 있습니다. 이 "렌더러 전용" 설정에서는 라이브러리가 네트워크에서 완전히 숨겨집니다. 재생 대상만 발표되며 MusicBee는 자체적으로 재생을 제공하지 않습니다.


재생 동작

N02 - 5.1 FLAC 자동 다운믹스 안 됨

무엇: MediaServerDevice.GetEncodedFile의 채널 수 제한은 If StereoOnly OrElse Not isPcmData Then channelCount = 2였습니다. Not isPcmData 절은 소스 채널 수에 관계없이 모든 비-PCM 트랜스코딩(FLAC, MP3, AAC, Ogg)을 스테레오로 자동으로 다운믹스하여 소스가 5.1 FLAC일 때 5.1 지원 렌더러를 무력화했습니다. 이제 두 번째 절은 FLAC을 제외합니다. Not isPcmData AndAlso encoder.Codec <> FileCodec.Flac. FLAC 5.1은 통과합니다. MP3/AAC/Ogg는 MusicBee의 해당 형식에 대한 명령줄 인코더가 2채널 입력을 예상하므로 여전히 스테레오로 강제됩니다.

왜: 5.1 FLAC 소스를 FLAC 출력으로 트랜스코딩하는 전체 목적은 다중 채널 믹스를 보존하는 것입니다. 자동 다운믹스는 서라운드 청취에 FLAC 트랜스코딩 옵션을 쓸모없게 만들었습니다. N02를 사용하면 올바르게 작동합니다.


아키텍처

원본 플러그인에는 없는 대규모 라이브러리에서 포크를 실행 가능하게 만드는 구조적 변경.

N03 - 지연 (주문형) 탐색 트리

무엇: 원본 플러그인은 MusicBee 시작 시 전체 탐색 트리를 구축했습니다. 모든 트랙을 열거하고, 파일당 전체 Library_GetFileTags를 수행하고, 전체 컨테이너 계층 구조를 HTTP 포트를 열기 전에 조립했습니다. 실제 라이브러리(5만 개 이상의 트랙, 5400개의 팟캐스트 에피소드, 수백 개의 스테이션)에서는 몇 분의 콜드 스타트가 발생하며, 클라이언트가 열지 않는 분기를 포함하여 트리가 RAM에 영원히 남아 있습니다. 이 포크는 미리 아무것도 구축하지 않습니다. 루트는 엔드포인트당 하나의 L: 접두사가 붙은 자리 표시자(L:music, L:podcast, L:filter:…)를 노출합니다. 각 수준은 클라이언트가 탐색할 때만 계산되며(LazyBrowseEnsureLazyEndpointInMemory → 수준별 캐시), 라이브러리 변경 알림은 캐시를 지웁니다(SetLibraryDirty).

왜: 콜드 스타트는 본질적으로 즉각적입니다. MusicBee가 플러그인 초기화를 완료할 때쯤 HTTP 포트가 열리며, 메모리는 라이브러리 크기가 아니라 탐색된 내용에 비례하여 유지됩니다. 절충: 엔드포인트로의 첫 번째 탐색은 로드 비용을 지불합니다. 재진입은 다음 라이브러리 변경까지 캐시됩니다. 이것이 다른 모든 것이 의존하는 기반입니다. 전체 노트: FIXES.md.


네트워킹 및 견고성

HTTP 서버의 바인딩 경로 강화. 원본 플러그인은 포트를 사용할 수 없을 때 자동으로 종료됩니다.

N04 - 자체 복구 HTTP 포트 바인딩

무엇: 플러그인의 HTTP 서버는 구성된 포트를 사용할 수 없을 때 더 이상 종료되지 않습니다. 세 가지 연결된 변경 사항:

  1. 바인딩 실패 시 자동 대체. HttpServer.Start는 구성된 포트를 시도하고, SocketException 발생 시 최대 20개 포트를 위로 스캔하여 첫 번째 사용 가능한 포트를 찾습니다. 실제로 바인딩된 포트는 새로운 Plugin.boundServerPort에 기록되며, 서버를 광고하는 모든 것 - SSDP LOCATION URL(NOTIFY + M-SEARCH 응답), 장치 URL(PrimaryHostUrl), 라우터 포트 전달, SSDP/제어 지점 자체 필터 -은 이제 Settings.ServerPort 대신 boundServerPort를 읽습니다. UPnP 클라이언트는 SSDP를 통해 실제 포트를 검색하므로 이동된 포트는 렌더러에 투명합니다.
  2. 사용자 알림. 대체가 발생하면(저장된 포트가 사용 중인 포트가 아님) 지역화된 MessageBox(WarnPortInUse)는 사용자에게 실제로 서비스 중인 포트와 장치가 여전히 해당 포트를 찾을 수 있음을 알려줍니다. 플러그인이 헤드리스로 실행되고 대화 상자 내 메시지는 이미 문제를 의심하는 사람만 볼 수 있기 때문입니다.
  3. 다시 시작 복구. RestartServer(설정 저장 다시 시작 경로)는 Plugin.controller / Plugin.server를 맹목적으로 역참조했습니다. 초기 Initialise가 생성하기 전에 예외를 발생시킨 경우(실패한 바인딩으로 인해 발생한 것과 동일), 다음 설정 저장 시 NullReferenceException이 발생하여 플러그인이 반쯤 죽은 상태로 남았습니다. 이제 Nothing일 때 다시 생성하고 시작하므로 작동하는 포트를 저장하면 전체 MusicBee 다시 시작 없이 플러그인이 다시 활성화됩니다.

왜: 트리거는 실제 사용자 사고였습니다. 이전 기본 포트 49382는 Windows 동적 범위(49152-65535)에 있으며, Hyper-V/WSL2/Docker/WinNAT은 각 부팅 시 이동하는 큰 블록을 예약합니다. 따라서 몇 달 동안 작동했던 컴퓨터에서 WSAEACCES("액세스 금지")로 바인딩이 실패했습니다. 기본값을 사용 가능한 포트로 변경한 다음 Serviio(새 포트에 이미 있는 별도의 DLNA 서버)와 충돌하여 WSAEADDRINUSE로 실패했습니다. 각 실패는 Initialise에서 삼켜져 플러그인이 자동으로 종료된 다음 다음 설정 저장 시 NRE가 발생했습니다. N04 이후 포트 충돌은 자체 복구됩니다. 서버는 다음 사용 가능한 포트에서 계속 실행되고 사용자에게 알려지며 클라이언트는 다시 검색합니다. 전체 플러그인을 종료하는 대신.

구현:

  • 기본 포트 49382 → **9779**로 이동(동적 범위 아래이므로 Windows에서 자동으로 예약하지 않음; 알려진 미디어 서버 기본값이 아님) 세 가지 ServerPort 선언 + 설정 구문 분석 실패 대체에 모두 적용.
  • Plugin.boundServerPort(새로운 공유 필드)는 활성 수신 포트를 보유합니다. activeServerPort구성된 스냅샷으로 유지되므로 "다시 시작 필요" 배지 논리가 대체 시 잘못 트리거되지 않습니다.
  • HttpServer.PortScanRange = 20; 스캔은 첫 번째 성공적인 TcpListener.Start()에서 중지되며 모든 시도가 실패한 경우에만 마지막 예외를 발생시킵니다.
  • 새로운 EN 리소스 키 WarnPortInUse (번역은 게시 시간 로케일 통과를 따릅니다).

N05 - 멀티캐스트 그룹을 통한 SSDP 알림 (VPN / 지점 간)

무엇: SSDP 알림은 IP 브로드캐스트 주소 대신 UPnP 멀티캐스트 그룹(239.255.255.250)으로 전송됩니다. SSDP 검색 응답이 서버 다시 시작과 경쟁할 때 기록되는 무해한 "폐기된 개체에 액세스할 수 없습니다" 오류도 억제됩니다.

왜: 지점 간 / VPN 네트워크 어댑터에서는 IP 브로드캐스트가 적용되지 않습니다. 이전 브로드캐스트 전송은 "잘못된 인수"로 실패하고 알림이 누락되어 플러그인이 해당 링크의 클라이언트에게 보이지 않았습니다. 적절한 멀티캐스트 그룹에 알림을 보내면 해당 어댑터에서 검색이 수정됩니다.

라이브러리 탐색

이것들은 이 포크에 포함되었으며 원본 플러그인에는 없습니다. 실제 UPnP 클라이언트에서 플러그인의 자체 출력을 실제로 탐색하여 얻었습니다.

N06 - 필터 기반 라이브러리 노출

무엇: MusicBee의 필터 탭(사용자 MusicBee 폴더의 .xautopf 파일)은 플러그인 라이브러리에서 UPnP 루트 컨테이너가 됩니다. 각 필터의 트랙은 AlbumArtistSort → Album → Tracks 계층 구조로 탐색할 수 있습니다.

왜: 큐레이션된 MusicBee 필터(예: "별 5개 트랙", "최근 추가됨", "클래식 → 바로크")를 사용하는 사용자는 UPnP 클라이언트에서 플러그인을 탐색할 때 해당 필터를 찾을 것으로 예상합니다. 원본 플러그인은 원시 라이브러리 트리만 노출했습니다.


N07 - SortAlbumArtist 필드 연결

무엇: 플러그인은 이제 MusicBee의 MetaDataType 165(정렬 앨범 아티스트)를 읽고 이를 사용하여 탐색 보기에서 아티스트를 그룹화/정렬합니다.

왜: 하이파이 브라우저와 오디오 애호가는 라이브러리를 정리하기 위해 정렬 아티스트 이름("Ludwig van Beethoven" 대신 "Beethoven, Ludwig van")을 사용합니다. 진지한 청취자에게는 표준적인 기대입니다. 두 업스트림 모두에서 누락되었습니다.


N08 - 다중 값 AlbumArtist 처리

무엇: 앨범의 AlbumArtist 필드에 "; "로 구분된 여러 아티스트(예: "yaiol; Ars Ricercata")가 포함된 경우, 트랙은 이제 이름을 결합한 단일 프랑켄슈타인 아티스트 아래가 아니라 탐색 보기에서 아티스트 아래에 나타납니다.

왜: 협업 앨범 및 컴필레이션은 모든 협업자 아래에 표시되어야 합니다. 이것이 없으면 앨범을 찾는 검색 경로의 절반이 손상됩니다.


N09 - 앨범 컨테이너 아트워크 (upnp:albumArtURI)

무엇: DIDL Browse 응답의 앨범 컨테이너 노드에는 이제 앨범 표지를 가리키는 upnp:albumArtURI 요소가 포함됩니다.

왜: 이것이 없으면 UPnP 클라이언트의 탐색 보기에서 모든 앨범이 앨범 표지 대신 일반 아이콘을 표시합니다. 탐색을 위한 시각적 단서; 모든 최신 하이파이 브라우저에서 예상됩니다.


N10 - 필터 앨범 내 트랙 순서

무엇: 필터로 노출된 앨범 내의 트랙은 이제 디스크 번호, 그 다음 트랙 번호로 정렬됩니다.

왜: 표준 앨범 순서. 명시적인 정렬이 없으면 트랙은 필터가 반환하는 순서대로 돌아왔으며, 일반적으로 무작위로 보였습니다.


N11 - 재생 목록 폴더 트리 수정

무엇: LoadLibraryPlaylists 함수(원래 Steven Mayall, ~2014)는 새로 생성된 재생 목록 폴더로 내려가지 못했습니다. 각 폴더의 첫 번째 재생 목록과 모든 하위 폴더는 루트 수준에서 고아로 끝났습니다.

왜: 11년 동안 원본 플러그인에 존재했습니다. BubbleUPnP를 열고 재생 목록을 클릭한 지 30초 이내에 볼 수 있었습니다. yaiol에서는 트리 구성 중에 새로 생성된 폴더로 올바르게 재귀하여 수정되었습니다.


N12 - XML-불법 제어 문자 정리

무엇: C0 제어 문자(예: 잘못된 인코딩 통과로 인한 0x19 - UTF-8 → Latin-1 → 0x990x19로 다시 자르기)를 포함하는 태그가 있는 트랙은 잘못된 트랙이 페이지 매김된 배치에 들어가면 전체 Browse 응답이 Action Failed로 실패했습니다.

왜: XML 1.0은 대부분의 C0 제어 문자를 금지하며, XmlWriter는 이를 쓰도록 요청받으면 예외를 발생시킵니다. 원본 플러그인에 존재했습니다. XmlConvert.IsXmlChar를 통해 모든 Library_GetFileTags 종료 지점에서 잘못된 문자를 제거하여 수정되었습니다.


N13 - 페이지 매김된 Browse에서 라디오 목록 결정론적

무엇: 라디오 컨테이너에 대한 Browse는 일반 파일 목록 분기로 떨어졌고, 이는 모든 호출에서 files.Sort(AlbumFileComparer)를 호출했습니다. 라디오 항목은 앨범/디스크/트랙 태그가 비어 있으므로 모든 비교는 0을 반환했습니다. List(Of T).Sort는 불안정하여 호출할 때마다 다른 순서를 생성했습니다. UPnP 제어 지점은 페이지 매김(BubbleUPnP는 0..15를 가져온 다음 16..끝을 가져옴)합니다. 두 호출 사이에 목록이 재정렬되어 일부 스테이션은 두 페이지 모두에 나타나고(중복) 일부는 어느 페이지에도 나타나지 않아(누락) 새로 고칠 때마다 무작위로 보였습니다.

왜: 원본 플러그인에 존재했습니다(작성자는 UPnP를 통해 라디오를 탐색하지 않았습니다). 여기서는 Browse에 전용 ContainerCategory.Radio 분기를 사용하여 수정되었으며, 호출당 정렬은 없습니다. radioFiles는 로드 시 제목별로 한 번 정렬됩니다(안정적). 페이지 매김된 탐색은 이제 결정론적 순서를 봅니다. 페이지 1과 페이지 2는 서로 다릅니다.


N14 - UPnP Search 앨범 클래스는 앨범 컨테이너를 반환합니다.

무엇: 앨범 클래스 쿼리(upnp:class = "object.container.album.musicAlbum", 예: BubbleUPnP의 "랜덤 앨범")에 대한 UPnP Search는 앨범 컨테이너 대신 전체 트랙 목록을 반환하여 클라이언트가 앨범을 0개 표시했습니다. 원본 핸들러는 괄호 안의 기준만 구문 분석한 다음 요청된 클래스에 관계없이 모든 트랙을 덤프했습니다.

왜: 여기에서 수정되었습니다. 앨범 클래스 쿼리는 이제 고유한 앨범(AlbumArtist+Album으로 그룹화됨)을 열거하고 각각을 표지 아트가 있는 적절한 musicAlbum 컨테이너로 내보냅니다. Salb<idx> 가상 ID 공간을 통해 주소 지정할 수 있으므로 클라이언트가 결과로 드릴다운하여 재생할 수 있습니다.


N15 - 클릭 스루가 있는 작동하는 범위 인식 UPnP 검색

무엇: 원본은 검색 기능이 없다고 광고하여(GetSearchCapabilities는 비어 있음) 클라이언트가 검색을 보내는 것조차 거부했습니다. 그리고 이전 백엔드는 musicFiles에서 읽었으며, 지연 트리 시대에는 영구적으로 비어 있었습니다. 이 포크는 실제 검색 가능한 속성을 광고하고, 지연 라이브러리에 대해 제목별 트랙 및 제목별 앨범을 구현하며(HandleLazySearch), 실제 컨테이너 ID가 전송될 때 쿼리 범위를 클라이언트의 현재 분기로 지정하고(그렇지 않으면 L:music을 대체하여 상단 바 검색이 팟캐스트/라디오/오디오북 노이즈를 끌어들이지 않도록 함), Browse 초기 분기가 앨범의 트랙으로 다시 매핑하는 합성 Ssrch_alb_* ID를 통해 앨범 결과를 클릭 가능하게 만듭니다. (앨범 클래스 결과가 컨테이너로 표시되는 부분은 N14입니다.)

왜: BubbleUPnP의 검색은 "라이브러리가 검색을 지원하지 않습니다"에서 유용하고 범위가 지정된 재생 가능한 결과를 반환하는 것으로 바뀌었습니다. 전체 디자인 + 거부된 접근 방식: SEARCH.md.


N16 - UPnP 캐시 무효화 (SystemUpdateID)

무엇: 원본은 상수 SystemUpdateID=0을 반환했습니다. 이는 UPnP ContentDirectory 캐시 무효화 계약이므로 사양 준수 클라이언트(BubbleUPnP)는 라이브러리를 변경되지 않는 것으로 취급했습니다. 오래된 탐색 결과, URL 스키마 변경 후 404 썸네일, "변경 사항을 보려면 MusicBee를 두 번 다시 시작"하는 과정. 이 포크는 로드 시 에포크 초에서 SystemUpdateID를 시드하고(따라서 모든 다시 시작은 이전보다 엄격하게 앞섭니다) 모든 라이브러리 변경 및 설정 변경 시 이를 증가시킵니다(SetLibraryDirty / ResetCacheBumpSystemUpdateId).

왜: 클라이언트는 다음 탐색 시 편집, 새 파일 및 설정 변경 사항을 안정적으로 가져옵니다. 알려진 제한: 구독된 클라이언트는 GENA를 통해 새 값을 적극적으로 다시 푸시하지 않습니다(향후 작업으로 보류됨). 다음 탐색 시 여전히 새 값을 봅니다.


N17 - 팟캐스트 구독 아트워크

무엇: 팟캐스트 타일에 이미지가 표시되지 않았습니다. 모든 /PodcastThumbnail/ 요청이 404 오류를 반환했습니다. 두 가지 버그가 겹쳤습니다. 해결 체인이 MusicBee의 실제 아트워크 캐시(%LocalAppData%\MusicBee\MusicBee\InternalCache\Subscriptions\<name>.jpg, 데스크톱 UI가 로드되는 위치)를 확인하지 않았고, HTTP 계층의 이스케이프 해제+소문자 변환이 피드 URL 경로 키를 마지막 경로 세그먼트까지 손상시켰습니다. 이 포크는 MB의 InternalCache에서 아트워크를 해결하고 HTTP 계층에서 손상되지 않는 URL 안전 슬러그(PodcastSlug / podcastSubIdBySlug)를 통해 조회를 라우팅합니다.

왜: 구독 아트워크가 이제 탐색 보기에 렌더링됩니다(이전에 404 오류를 반환했던 22개의 요청이 모두 해결됨).


N18 - 계층적 (구분된) 태그 탐색

무엇: 라이브러리 옵션 탭에서 모든 필드를 계층적으로 표시하고 한 문자 구분 기호를 지정할 수 있습니다(추가/제거 기능이 있는 필드 선택기 + 구분 기호 상자, 플러그인 설정에 유지됨). 그룹화/로 설정하고 Jazz/Cool Jazz와 같은 값을 지정하면 단일 평면 항목 대신 Jazz › Cool Jazz로 탐색됩니다. 브랜치에 정확히 태그가 지정된 트랙(그냥 Jazz)은 자체 [Jazz] 노드를 가져와 아무것도 숨겨지지 않으며, 단일 자식이 있는 브랜치는 자체적으로 축소되고, ;는 MusicBee 자체의 다중 값 구분 기호이므로 구분 기호로 거부됩니다.

왜: 사용자가 이미 단일 필드에 인코딩한 심층 태그 분류(장르 트리, 분위기 계층, "클래식/바로크/협주곡")는 사용자가 처음부터 끝까지 읽어야 하는 슬래시로 구분된 문자열의 평면 벽 대신 태그가 설명하는 트리로 마침내 탐색됩니다.


N19 - 그룹화 필드로 레이블이 지정된 단일 루트 경로

무엇: 루트의 단일 탐색 경로는 전체 짧은 경로 대신 그룹화 필드(예: "장르")로 레이블이 지정되어 병합된 첫 번째 필드 그룹의 이름 지정 방식과 일치합니다.

왜: 탐색 트리가 일관되게 읽힙니다. 루트 항목이 단독으로 있든 형제 항목과 병합되었든(N20) 하나의 이름 지정 규칙이 적용됩니다. 병합된 이웃이 깔끔한 필드 이름을 표시하는 동안 단독 루트 항목이 장황한 내부 경로를 표시하는 대신.


N20 - 첫 번째 필드를 공유하는 탐색 경로 병합

무엇: 동일한 첫 번째 필드를 공유하는 두 개의 탐색 경로("장르 / 정렬 앨범 아티스트" 및 "장르 / 팟캐스트 인물")는 단일 장르 루트 폴더로 축소되어 장르 값을 먼저 나열한 다음 두 가지 보기로 분할됩니다. 루트에 거의 중복되는 두 개의 "장르 / ..." 항목이 나란히 있는 대신.

왜: 공통 필드 아래에 여러 관련 보기를 중첩한 사용자는 루트가 거의 동일한 최상위 항목으로 어수선하게 표시되는 것을 보았습니다. 이를 병합하면 루트를 읽기 쉽게 유지하고 관련 보기를 해당 위치(공유 필드 아래)에 그룹화합니다.


N21 - 카테고리 유형 탐색 경로 (표준 / 라디오 / 팟캐스트)

무엇: 모든 탐색 경로는 표준, 라디오 또는 팟캐스트 카테고리로 유형이 지정됩니다. 템플릿 목록은 이 세 섹션으로 그룹화되며, 각 템플릿의 필드 선택기는 해당 카테고리 데이터가 실제로 제공할 수 있는 필드만 제공하며, 템플릿은 보기 트리에서 일치하는 노드에만 적용할 수 있습니다(호환되지 않는 노드는 회색으로 표시되어 선택할 수 없음). 예약된 라디오 및 팟캐스트 템플릿은 삭제할 수 없으므로 해당 카테고리 섹션은 사라지지 않습니다.

왜: 유형 지정이 없으면 사용자는 자동으로 비어 있는 레이아웃을 만들 수 있습니다. 라디오 방송국에는 "앨범"이 없고, 팟캐스트 에피소드에는 "앨범 아티스트"가 없습니다. 그리고 UPnP 클라이언트에서 죽은 폴더로 탐색해야만 이를 발견할 수 있습니다. 필드 메뉴와 적용 대상을 카테고리의 실제 데이터로 제한하면 비어 있는 레이아웃을 만들 수 없습니다.


N22 - 팟캐스트를 게시 연도별로 그룹화

무엇: 각 팟캐스트 에피소드의 게시 날짜가 읽혀지므로, 연도 수준이 있는 팟캐스트 탐색 경로는 에피소드를 단일 "알 수 없음" 아래로 축소하는 대신 연도별로 분류합니다.

왜: 대규모 팟캐스트 구독은 플러그인이 에피소드별 게시 날짜를 확인하지 않아 모든 에피소드가 날짜 없는 하나의 더미에 쌓이는 대신 라이브러리의 나머지 부분처럼 연도별로 탐색할 수 있게 됩니다.


N23 - 단일 결과 그룹화 수준 축소

무엇: 단일 값으로 해결되는 그룹화 수준(LP만 만든 아티스트에 대해 "LP"만 표시하는 레코드 유형 수준 또는 단일 문자가 있는 문자 수준)은 자동으로 건너뛰어 사용자를 해당 내용으로 바로 이동시킵니다.

왜: 정확히 하나의 폴더를 포함하는 폴더를 탐색하는 것은 순수한 마찰입니다. 단일 선택 수준을 축소하면 사용자가 도달할 수 있는 내용을 변경하지 않고 불필요한 클릭을 제거합니다.


N24 - MusicBee의 날짜 필드에 대한 연도 그룹화/검색

무엇: 연도 조건은 더 이상 MusicBee의 전체 날짜 "연도" 필드를 네 자리 값으로 쿼리하지 않으며, 하드 코딩된 연도 필드 별칭이 사라져 모든 그룹화 필드가 이제 경로 정의에서 일반적으로 해결됩니다.

왜: 연도 태그에 전체 날짜가 포함된 라이브러리의 경우, 연도별로 그룹화하거나 검색하면 이전에 아무것도 반환되지 않았습니다. 네 자리 쿼리가 전체 날짜 필드와 일치하지 않았기 때문입니다. 올바른 필드를 쿼리하면 연도 그룹화 및 검색이 다시 트랙을 찾습니다.


N25 - "연도" 및 "연도 (yyyy)" 그룹화 필드 분리

무엇: 앨범 그룹화 및 탐색 경로는 이제 MusicBee 자체의 두 가지 연도 필드인 연도(전체 날짜 태그)와 연도 (yyyy)(네 자리 연도만)를 모두 노출하므로 사용자는 앨범 그룹화 또는 탐색 경로를 정의할 때 둘 중 하나를 선택할 수 있습니다.

왜: 두 필드는 MusicBee에서 다른 의미를 가지며, 이를 축소하면 그 구별이 사라졌습니다. 둘 다 표시하면 사용자가 의도한 대로 한 해의 모든 릴리스를 함께 모으거나(yyyy) 정확한 날짜 순서를 유지할 수 있습니다(전체 연도 태그).


N32 - 고정된 필터와 재생 목록을 루트에서 종류별로 그룹화

무엇: 고정한 필터는 이제 찾아보기 루트의 필터 폴더 바로 아래에, 고정한 재생 목록은 재생 목록 폴더 바로 아래에 나타납니다. 모든 고정 항목이 루트 끝에 한 덩어리로 모이는 대신, 고정된 각 바로 가기는 자기 종류와 함께 자리합니다.

왜: 바로 가기를 더 많이 고정할수록 필터와 재생 목록이 뒤섞인 끝부분의 한 덩어리는 훑어보기 어려워지고, 각 바로 가기를 자신이 속한 폴더에서 떼어놓게 됩니다. 고정 항목을 자체 카테고리 아래에 그룹화하면 루트가 읽기 쉽게 유지되고 각 바로 가기가 관련 항목 옆에 놓입니다.

설정 대화 상자 및 패키징

N26 - 섹션화된 설정 대화 상자

무엇: 환경설정 페이지는 일반 / 재생 / 라이브러리 / 장치 프로필 / 진단 섹션이 있는 왼쪽 탐색 레이아웃을 얻었습니다.

왜: 원본은 모든 설정의 단일 길고 평평한 목록이었습니다. 이를 만든 개발자에게는 괜찮았지만 다른 모든 사람에게는 혼란스러웠습니다. 섹션화는 관련 옵션을 그룹화하고 대화 상자를 최신 앱 설정처럼 느끼게 합니다.


어셈블리 + 플러그인 이름 변경 (F-id 없음 - 패키징 참고)

무엇: 컴파일된 DLL은 mb_UPnP_yaiol.dll로 명명되며 플러그인은 자체를 "MusicBee UPnP (yaiol)"로 보고합니다. 원본 mb_Upnp.dll과 다릅니다.

왜: 사용자는 yaiol을 원본 플러그인과 함께 설치하고 동작을 나란히 비교할 수 있습니다.


배지 시스템 - 런타임 상태 표시 (F40 뒤의 메커니즘)

무엇: 중요한 런타임 조건을 설정 대화 상자에 눈에 띄는 색상 배지로 표시하는 일반적인 UI 패턴. 현재 인스턴스:

  • ⚠ 최대 연결 (N04) - MusicBee 시작 이후 최대 연결 제한에 한 번 이상 도달했을 때 발생합니다. 고정 세션 플래그 Plugin.MaxConnectionsHit. 사용 가능한 슬롯이 없을 때 WaitOnSendBarrier 내에서 설정됩니다.
  • ⚠ 다시 시작 필요 - 저장된 설정이 적용되려면 MusicBee 다시 시작이 필요할 때 발생합니다. 고정 세션 플래그 Plugin.RestartRequired. 새 영구 값이 런타임 스냅샷(Plugin.activeMaxConnections, Plugin.activeServerPort, Plugin.activeIpAddress)과 다를 때 대화 상자의 저장 핸들러에서 설정됩니다. 다시 시작이 필요한 설정은 실제로 핫 리로드할 수 없는 설정으로 제한됩니다. HTTP 서버 바인딩 매개변수 및 Initialise에서 한 번 구축된 SemaphoreSlim.

왜: 플러그인의 로그 파일은 기술 사용자가 디버깅하는 데는 좋지만, "장치 소리가 이상하다" 또는 "재생이 느리다"를 보는 비기술 사용자는 진단 → 로그 보기를 열지 않을 것입니다. 배지는 사용자가 무언가 일어났다는 것을 알아야 하는 경우를 포착하고 플러그인을 다음에 열 때 이를 표시합니다. 아무것도 읽지 않고도 발견할 수 있습니다.

향후 재사용 가능:

  • 프로필 불일치 감지됨 (장치 사용자 에이전트가 어떤 프로필과도 일치하지 않아 일반으로 대체됨).
  • NextURI 백오프 트리거됨 (F13 - 불안정한 장치에서 세션 동안 무음 재생 비활성화됨).
  • 라이브러리 스캔 실패 / 부분적.
  • 세션 중간에 렌더러 연결 끊김.
  • "한 번 발생했고 사용자가 알아야 함"이 "1000줄의 다른 줄 사이에 자동으로 기록됨"보다 나은 다른 모든 조건.

구현 규칙:

  • 배지 레이블은 대화 상자 수준에 있으므로(어떤 패널 내부가 아님) 사용자가 어떤 섹션에 있든 관계없이 표시됩니다.
  • 저장/취소 근처의 하단 행에 배치됩니다(현재: y=410 가로로 쌓임).
  • 각 배지에는 Plugin에 해당 고정 세션 플래그가 있으며, 조건이 발생하면 True로 전환되고 MusicBee 다시 시작 시에만 재설정됩니다.
  • 리소스: <Condition>Badge (레이블 텍스트, ⚠ 접두사) + <Condition>BadgeTip (원인 + 해결책을 설명하는 도구 설명).
  • "저장된 설정에 다시 시작 필요" 배지의 경우, Plugin.Initialise()에서 런타임 스냅샷을 찍고 대화 상자 저장 핸들러에서 Settings.SaveSettings()Settings.*와 비교합니다.

N27 - 취소 시 경로/템플릿 편집 내용 삭제

무엇: 설정 대화 상자 내에서 경로 및 템플릿에 대한 편집 내용은 이제 사용자가 취소를 클릭하면 자동으로 적용되는 대신 삭제되며, 세션 중에 제거된 예약된 템플릿은 다시 생성됩니다. (템플릿은 편집될 때 실시간으로 저장됩니다. 경로 탭에는 별도의 저장 버튼이 없습니다.)

왜: 취소는 취소를 의미해야 합니다. 이전에는 경로/템플릿 변경을 실험하고 취소한 사용자가 변경 사항이 이미 커밋되었음을 발견했으며, 각 변경 사항을 수동으로 다시 실행하는 것 외에는 실행 취소할 방법이 없었습니다.


N28 - 필드 선택기 메뉴의 리터럴 앰퍼샌드

무엇: 이름에 "&"가 포함된 필드(예: "Mood & Context")는 Alt-니모닉 접두사로 삼키는 대신 필드 선택기 메뉴에 앰퍼샌드를 리터럴로 렌더링합니다.

왜: 앰퍼샌드가 있는 필드 이름이 잘못 표시되어(문자가 사라지고 다음 문자가 가속기가 됨) 메뉴 항목을 인식하기 어려웠습니다.


N29 - 안정적이고 번역되지 않은 설정 창 제목

무엇: 설정 창 제목은 "MusicBee UPnP Plugin" 브랜드 문자열로 고정되며 더 이상 인터페이스 언어에 따라 변경되지 않습니다. 언어별 DialogTitle 문자열은 모든 로케일 번들에서 제거되었습니다.

왜: 언어별로 문구가 변경되는 창 제목은 이점이 없는 번역 가능한 표면이었습니다. 제목은 브랜드 마크입니다. 이를 고정하면 모든 곳에서 안정적이고 일관되게 유지됩니다.

현지화

원본 플러그인은 영어 전용입니다. 이 포크는 완전히 현지화 가능합니다. 모든 사용자 대면 문자열은 리소스 번들을 통해 흐르며, 플러그인은 MusicBee 자체의 UI 언어를 자동으로 감지합니다.

N30 - 다국어 UI (번역 보류 중)

무엇: 현지화 메커니즘이 완료되어 배송 중입니다. Localisation.vbMusicBee3Settings.ini(endonym <SystemLanguage>)에서 MusicBee의 선택된 언어를 읽고 일치하는 .NET 문화를 스레드에 적용하므로 My.Resources.Resources.*는 현지화된 문자열을 반환합니다. 모든 사용자 대면 레이블/버튼/메시지는 리소스 키에 연결됩니다(디자이너 컨트롤은 ApplyDesignerExtras + sync-en-locale.js를 통해; WarnPortInUse와 같은 런타임 문자열은 수동으로 추가됨). 아직 완료되지 않은 것은 실제 번역입니다. 영어 소스 번들(Resources.resx)만 존재합니다. 다른 언어에 대한 위성 번들은 플러그인이 기능적으로 완료될 때 한 번의 일괄 처리로 생성됩니다(문자열이 여전히 변경되는 동안 부분적으로 번역하는 것은 노력을 낭비합니다).

대상 언어 (MusicBee 자체가 제공하는 세트, endonymToCulture에 의해 1:1로 일치하여 플러그인이 MusicBee의 언어를 자동으로 따릅니다):

아랍어 (ar) 체코어 (cs) 독일어 (de) 그리스어 (el)
스페인어 (es) 프랑스어 (fr) 헝가리어 (hu) 이탈리아어 (it)
한국어 (ko) 네덜란드어 (nl) 노르웨이어 (nb) 폴란드어 (pl)
포르투갈어 BR (pt-BR) 포르투갈어 PT (pt-PT) 스웨덴어 (sv) 터키어 (tr)
우크라이나어 (uk) 러시아어 (ru) 일본어 (ja) 중국어 간체 (zh-CN)
중국어 번체 (zh-TW) 영어 (en, 원본)

변형 정책 (작업 공간 로케일 규칙에 따라): 어휘/스크립트가 실제로 다르기 때문에 PTZH는 별도의 번들로 분할됩니다(pt-BR/pt-PT, zh-CN/zh-TW). EN은 단일 번들입니다. MusicBee의 "English(US)"(en-US)는 .NET의 문화 체인을 통해 en으로 대체되므로 별도의 US 번들은 생성되지 않습니다. ES 및 FR도 마찬가지로 단일 로케일입니다.

왜: UPnP 플러그인의 설정("원시 PCM 사용 안 함", "리틀 엔디언 PCM 강제", 포트 대체 경고)은 모국어로도 충분히 난해합니다. 영어를 강제하는 대신 MusicBee 자체의 UI 언어를 따르는 것은 비영어 사용자가 구성할 수 있는 도구와 구성할 수 없는 도구의 차이입니다. 두 업스트림 모두 이를 시도하지 않았습니다.


N31 - 도움말 링크가 전체 인터페이스 언어로 열림

무엇: 플러그인에서 도움말 링크를 열면 기본 언어로 축소되는 대신 사용자의 전체 인터페이스 언어(예: pt-BR, zh-CN)를 존중하고 더 명확한 업데이트 확인 식별자를 보냅니다.

왜: 지역 변형(브라질 포르투갈어, 중국어 간체)으로 MusicBee를 실행하는 사용자는 기본 언어 도움말 페이지로 보내졌습니다. 전체 문화를 전달하면 사용자가 사용하는 정확한 언어로 된 도움말 페이지로 이동합니다.

원본 플러그인에 대한 수정 및 개선 사항

핵심 프로토콜 및 재생

F01 - 업데이트된 기본 DLNA 장치 프로필

무엇: PlayStation 4, Xbox 360/One 및 최신 BubbleUPnP에 대한 새로운 기본 프로필을 제공하며, 해당 장치가 오늘날 실제로 지원하는 기능을 반영하는 기능 플래그(샘플 속도, 비트 깊이, 코덱)를 포함합니다.

왜: 원본 플러그인의 기본값은 2014년경에 고정되었습니다. PS4/Xbox/BubbleUPnP는 이후 고해상도 오디오 지원을 얻었습니다. 기본적으로 새 설치는 사용자가 장치 프로필 설정을 건드리지 않고도 이러한 장치에서 최고 수준으로 재생됩니다.


F02 - MediaRenderer:3을 광고하는 장치 제어

무엇: 플러그인은 렌더러의 UPnP 서비스 설명을 조사하여 MusicBee가 이를 구동할 수 있는지 여부를 결정합니다. 원본은 urn:schemas-upnp-org:device:MediaRenderer:1만 일치시켰습니다. 최신 장치는 :2 또는 :3을 광고합니다. F02는 일치를 확장합니다.

왜: 이것이 없으면 최신 Sonos / WiiM / Eversolo 장치는 동일한 프로토콜을 사용하더라도 MusicBee의 "재생 대상" 장치 목록에 대상으로 나타나지 않습니다. 단일 문자열 접두사 일치 수정으로 전체 최신 장치 세대가 잠금 해제됩니다.


F03 - "네이티브 스트림 강제" 프로필별 옵션 (기본값 ON)

무엇: 선택하면 플러그인은 트랜스코딩, DSP, ReplayGain 처리 없이 원본 파일 바이트를 장치로 보냅니다. 사용자가 선택한 원시 파일 그대로, 바이트 단위로(HTTP 프레이밍 제외).

왜: 포럼 증언에 따르면 이것이 가장 큰 재생 품질 향상입니다. 고음질 사용자는 비싼 렌더러를 구입하여 비트 완벽한 출력을 명시적으로 원합니다. 모든 DSP 터치는 그 목적을 무력화합니다. 대부분의 최신 장치는 사용자가 던지는 모든 코덱을 처리하고 ReplayGain/EQ는 선택 사항이어야 하므로 기본적으로 ON입니다. 프로필별이므로 오래된 Xbox에는 트랜스코딩을 유지하면서 고음질 DAC에는 네이티브를 보낼 수 있습니다.


F04 - "트랜스코딩 강제" 프로필별

무엇: 네이티브 코덱 지원 여부에 관계없이 모든 스트림을 이 장치로 트랜스코더를 통해 강제하는 프로필별 재정의. F03(네이티브 스트림 강제)의 역. F03과 상호 배타적입니다. 둘 중 하나가 켜지면 UI가 자동으로 다른 하나를 선택 해제합니다.

왜: 단일 전역 토글은 프로필별 네이티브 스트림 강제(F03)와 모순될 것입니다. 실제 사례: 장치 A는 비트 완벽한 네이티브 스트림을 원하는 고음질 DAC입니다. 장치 B는 FLAC에서 멈추는 오래된 AV 리시버입니다. 전역 토글을 사용하면 사용자는 다른 장치를 희생하여 선택해야 합니다. 프로필별로 각 장치는 올바른 응답을 얻습니다.

구현:

  • StreamingProfile.ForceTranscoding As Boolean = False.
  • 영구 스키마가 v9로 상향 조정되었습니다. v9 이전 파일은 레거시 전역 값을 한 번 로드하고 모든 프로필에 복사하여 업그레이드를 통해 이전 동작을 유지합니다.
  • UI: 진단 패널에서 제거되고 장치 프로필 섹션에 ForceNativeStream 옆에 추가되었습니다. 양방향 상호 배제 핸들러(각각 CheckedChanged는 무한 루프를 피하기 위해 전환하기 전에 다른 하나를 구독 해제합니다).
  • 결정 지점: Settings.ForceTranscodingWriteAudioFileDIDLstreamingProfile.ForceTranscoding.

F05 - "리틀 엔디언 PCM 강제" 프로필별

무엇: PCM 스트림(L16/L24 MIME 유형)은 사양에 따라 빅 엔디언입니다. 일부 장치는 잘못 리틀 엔디언을 예상하고 적절한 빅 엔디언 데이터를 받으면 백색 소음을 재생합니다. F05는 프로필별로 바이트 순서를 전환합니다.

왜: 이것이 없으면 특정 장치는 정적 벽을 출력합니다. 증상은 극적이며 PCM 인코딩에 대한 지식 없이는 원인을 알 수 없습니다. 토글은 사용자에게 추측 및 확인 수정 기능을 제공합니다.


F06 - "원시 PCM 사용 안 함" 프로필별

무엇: 장치가 원시 PCM을 지원한다고 주장하면 플러그인은 이를 사용합니다. 일부 장치는 거짓말을 합니다. SOAP 핸드셰이크를 수락하지만 실제 원시 PCM 데이터를 왜곡하고 WAVE 컨테이너에 래핑된 PCM을 제대로 처리합니다. F06은 장치가 광고하는 내용에 관계없이 PCM-over-Wave를 강제합니다.

왜: 특히 특정 Marantz 모델은 원시 PCM을 광고하지만 WAVE만 작동합니다. 이것이 없으면 원시 PCM 스트림이 왜곡되어 나타나고 오류 메시지가 표시되지 않습니다.


F07 - "콘텐츠 길이" 프로필별

무엇: HTTP Content-Length 헤더에 보낼 값. 네 가지 옵션:

  • 기본값 - 알려진 경우 실제 바이트 수, 알 수 없는 경우 생략.
  • 없음 - 헤더를 절대 보내지 않음 (청크 인코딩만).
  • PCM 전용 - 원시 PCM에만 보내고 다른 모든 것은 생략.
  • 고정 - UInt32.MaxValue - 8192를 보냄 ("매우 큰 알 수 없는 길이"에 대한 센티넬).

왜: UPnP/DLNA 장치는 Content-Length에 대한 반응이 매우 다양합니다. 일부는 정확한 숫자가 필요하고, 일부는 스트림에서 이를 싫어하며, 일부는 버퍼링을 유지하기 위해 센티넬 "정말 큰" 값이 필요합니다. 이 문제는 나중에 PCM 전용에서 모든 출력 형식으로 확장되었습니다. 동일한 문제가 트랜스코딩된 MP3/AAC 스트림에서도 나타났기 때문입니다.


F08 - "NextURI 지우지 않음" 프로필별

무엇: 일반적으로 플러그인은 큐가 비워지면 장치의 큐에 있는 NextURI를 지웁니다(빈 URL로 SetNextAVTransportURI를 보냄). 일부 장치(특히 Denon)는 빈 NextURI를 "모든 것을 중지"로 해석하고 즉시 재생을 중단합니다. F08은 플러그인이 이를 지우는 것을 방지합니다.

왜: 이것이 없으면 Denon 소유자는 큐가 비워질 때 장치가 트랙 중간에 끊기는 것을 경험합니다. F08이 선택되면 장치는 오래된 NextURI를 메모리에 유지합니다(무해합니다. 다음에 무언가가 큐에 추가될 때 덮어쓰기만 됩니다).


F09 - 트랜스코딩 출력 형식으로 FLAC

무엇: 장치 프로필의 트랜스코딩 형식 드롭다운은 이제 PCM 16/24, MP3, AAC, Ogg와 함께 FLAC을 제공합니다. 이를 선택하면 BASS 인코더가 MusicBee의 표준 FLAC 변환 명령줄을 통해 라우팅됩니다(MP3/AAC/Ogg가 이미 사용하는 것과 동일한 메커니즘).

왜: FLAC을 잘 처리하지만 소스 코덱을 디코딩할 수 없는 장치(예: MusicBee의 WMA 라이브러리를 FLAC으로 변환하여 수신하는 Eversolo)의 경우, MP3/AAC가 오디오 데이터를 버리는 대신 무손실 품질을 보존합니다. 무손실 트랜스코딩 옵션 없이는 해결할 수 없었던 N02(5.1 다운믹스 제어)를 차단 해제합니다.

구현: Encoder.StartEncodeSelect Case Codec 블록에 한 줄 추가 - FLAC이 명령줄 기반 분기에서 MP3/AAC/Ogg에 합류합니다. UI 드롭다운은 6번째 옵션으로 "FLAC"을 얻습니다. SettingsDialog의 로드/저장 매핑은 FileCodec.FlacSelectedIndex = 5를 인식하도록 확장됩니다. MIME, DLNA 유형 및 인코딩 기능은 이전 작업(F21, F26)에서 ItemManager.GetMimes / GetDlnaType / GetEncodeFeature에 이미 연결되어 있었습니다.


무음 재생 (SetNextAVTransportURI)

F10 - SetNextAVTransportURI / NextURI 코어

무엇: 진정한 무음 재생. 장치가 UPnP 서비스 설명에서 SetNextAVTransportURI 지원을 광고하면 플러그인은 현재 트랙이 끝나기 전에 다음 트랙을 장치에 미리 큐에 추가합니다. 장치는 트랙 사이에 들리는 간격 없이 내부적으로 전환됩니다. CD 플레이어에서 듣는 것과 같습니다. 이것은 "연속 스트림" 해킹(모든 것을 하나의 긴 스트림으로 연결하고 트랙별 메타데이터를 잃는)이 아닙니다.

왜: 플래그십 Tier-2 기능. 연속 라이브 공연으로 녹음된 앨범(라이브 레코드, 클래식 악장, DJ 세트)은 트랙 사이에 0.5초의 침묵이 있으면 이상하게 들립니다. 이 문제를 제대로 해결하는 것은 이 포크에 있는 플래그십 기능입니다.

참고: 큐에 추가된 오디오는 streamHandle=0(라이브러리 가져오기 모드)을 사용하여 플러그인의 HTTP 서버를 통해 제공됩니다. 이는 MusicBee의 오디오 엔진이 큐에 추가된 트랙에 대해 루프에 있지 않음을 의미합니다. 절충: ReplayGain/DSP/EQ 효과는 다음 트랙에 적용되지 않습니다. "네이티브 스트림 강제"가 켜져 있을 때(기본값) 허용됩니다.


F11 - "NextURI 지원 비활성화" 프로필별

무엇: 장치가 SetNextAVTransportURI를 광고하더라도 이 확인란은 플러그인이 해당 광고를 무시하고 한 번에 한 트랙씩 재생으로 대체하도록 강제합니다.

왜: 일부 장치는 NextURI를 광고하지만 버그가 있는 구현(충돌, 불완전한 전환, 멈춤)을 가지고 있습니다. 각 손상된 장치를 역설계하는 대신 사용자는 "여기서 그냥 끄세요" 토글을 얻습니다.


F12 - 현재 재생 목록의 NextURI 수명 주기

무엇: MusicBee가 NowPlayingListChanged를 발생시키면 플러그인은 무음 전환을 위해 큐에 추가되어야 하는 것을 다시 평가합니다. NowPlayingList_GetNextIndex(1) + NowPlayingList_GetListFileUrl을 통해 MusicBee에 새 "다음" 트랙을 요청하고, 장치에 현재 큐에 있는 것과 비교하고(새 nextPlaySourceUrl 필드를 통해 추적됨), 변경된 경우 다시 큐에 추가합니다(또는 MusicBee가 다음 트랙이 없다고 말하면 큐를 지웁니다).

왜: F12가 없으면 사용자가 큐에 있는 트랙을 제거/재정렬할 때 장치는 오래된 NextURI를 계속 재생했습니다. 이는 모든 목록 변경에 다른 처리가 필요하기 때문에 역사적으로 여러 번의 반복이 필요했습니다. 우리는 NowPlayingList_GetNextIndex를 신뢰하여 단순화했습니다(이는 이미 셔플 및 전체 반복 랩어라운드를 존중함). 따라서 모든 변형은 동일한 비교를 통해 전달됩니다.

구현:

  • 새로운 nextPlaySourceUrl 필드는 큐에 있는 MusicBee 라이브러리 URL을 저장합니다(핸들 접미사가 있는 스트리밍 URL은 라이브러리 경로와 비교할 수 없습니다).
  • MediaRendererDevice에 새로운 Public Sub RefreshQueuedNextUri()가 있습니다. 세 가지 결과: NextURI가 큐에 없음 → 아무것도 하지 않음; 큐에 있는 것이 새 "다음"과 일치 → 아무것도 하지 않음; 큐에 있는 것이 다름 → 새 URL로 QueueNext 호출(또는 지우려면 QueueNext("") - 이는 F08 DoNotClearNextUri를 존중함).
  • Plugin.ReceiveNotificationNotificationType.NowPlayingListChanged 아래에 연결되었습니다.

F13 - NextURI 실패 백오프

무엇: 동일한 장치에서 4번 연속 SetNextAVTransportURI 실패 후, 플러그인은 MusicBee가 다시 시작될 때까지 해당 장치에 대한 무음 재생을 비활성화합니다.

왜: 장치가 NextURI에 대해 실제로 손상된 경우(간헐적인 SOAP 오류, 네트워크 결함), 플러그인은 그렇지 않으면 모든 트랙에서 계속 재시도할 것입니다. F13은 노이즈를 멈추고 자동으로 한 번에 한 트랙씩 재생으로 대체합니다.


F14 - 반복 모드 + NextURI 통합

무엇: F14는 OnAvTransportStatusCheck의 F15 전환 감지기에서 처리되는 두 가지 경우로 나뉩니다.

  • 모두 반복: MusicBee는 올바른 "랩" URL(목록 끝의 트랙 1)을 Plugin.QueueNext 자체에 전달합니다. 특별한 플러그인 로직은 필요하지 않습니다. 장치는 해당 URL로 전환되고 F15 감지기는 평소와 같이 Player_PlayNextTrack을 호출하여 MusicBee의 NPL 인덱스를 0으로 다시 랩합니다.
  • 하나 반복: MusicBee는 동일한 트랙 URL을 Plugin.QueueNext에 전달합니다. 장치는 해당 URL로 전환됩니다(새 스트림 핸들, 동일한 소스). F15 감지기는 이제 Player_GetRepeat()을 쿼리합니다. RepeatMode.One인 경우 Player_PlayNextTrack 호출을 건너뛰어 MusicBee가 NPL 인덱스를 루핑 트랙에서 벗어나지 않도록 합니다.

왜: 하나 반복 건너뛰기가 없으면 무음 전환 시 Player_PlayNextTrack을 호출하면 MusicBee가 목록의 다음 트랙으로 이동하여(하나 반복은 플레이어 UI의 트랙 끝 자동 진행 동작에만 영향을 미칩니다. 다음 트랙은 항상 앞으로 이동합니다) 하나 반복의 의미와 모순됩니다.

재생 횟수 주의사항: 하나 반복에서 재생 횟수 증가는 MusicBee 3.7.9563+가 루프 재생을 감지하는지에 따라 달라집니다. 이전 MusicBee 버전은 무음 반복을 올바르게 재생하지만 재생 횟수 증가를 놓칩니다. 문서화되었으며 차단되지 않습니다.


F15 - 트랙 전환 감지 상태 머신

무엇: 장치가 현재 트랙에서 NextURI로 내부적으로 전환될 때 플러그인은 이를 감지하고 MusicBee에 현재 재생 중인 인덱스를 진행하도록 알려야 합니다. 그렇지 않으면 MusicBee는 여전히 이전 트랙에 있다고 생각하고 재생 횟수 / UI / 스크로블링이 동기화되지 않습니다.

구현: 각 상태 타이머 틱에서 GetPositionInfo.TrackURI를 폴링합니다. 보고된 URI가 NextURI를 통해 큐에 추가한 URI와 일치하면 MusicBee에서 Player_PlayNextTrack을 호출하고 suppressNextSoapCall을 설정하여 결과 PlayToDeviceSetAVTransportURI를 다시 보내지 않도록 합니다(이는 무음 재생을 방해할 것입니다).

왜: F15가 없으면 장치는 다음 트랙을 재생하지만 MusicBee의 UI는 이전 트랙에 있다고 표시합니다. 혼란스럽고 스크로블링을 방해하며 재생 횟수 추적을 방해합니다. 트랙 전환 감지는 각 렌더러 브랜드가 URI 변경을 보고하는 시점에 고유한 특성(일부는 TRANSITIONING을 먼저 보고하고, 일부는 새 URI로 PLAYING으로 바로 건너뛰고, 일부는 그 사이에 짧은 STOPPED가 있음)이 있기 때문에 렌더러별로 길고 반복적인 작업이 필요합니다.

참고: 첫 번째 버전은 BubbleUPnP 렌더러에서 작동합니다. 장치별 예외는 B6에 남아 있습니다.


F16 - 무음 전환 시 팝 수정

무엇: 팝은 큐에 추가된 트랙의 소스 형식(샘플 속도 / 채널 / 코덱)이 현재 재생 중인 트랙과 다를 때 발생하여 장치의 DAC가 전환 시 다시 잠기도록 강제합니다. F16은 형식이 다를 때마다 큐에 추가될 때 발생하는 NextUri:FormatChange 진단을 추가하여 양쪽을 명명합니다. 따라서 팝을 듣는 사용자는 상관 관계를 파악할 수 있습니다.

진단은 또한 완화책을 지적합니다. 장치 프로필에서 트랜스코딩 강제를 선택합니다. 이는 모든 트랙을 단일 트랜스코딩 코덱/샘플 속도/비트 깊이로 균일화하여 소스 형식 차이를 완전히 제거합니다.

실제 트랜스코딩 일치 수정으로 연기된 이유: 구조적 수정(큐에 추가된 트랙을 재생 중인 트랙의 형식과 일치하도록 트랜스코딩)은 플러그인의 HTTP 서버 URL 스키마 변경이 필요합니다. 현재 /encode/{id}0.{ext}는 큐에 추가된 파일을 네이티브로 제공합니다. F16의 향후 v2는 형식별 /encode/{id}0_{rate}_{depth}.{ext} 경로를 추가하고 인코더를 통해 연결할 것입니다. 이는 ForceTranscoding이 충분하지 않은 경우 실제 장치에서 팝이 발생하면 수행할 가치가 있는 더 큰 아키텍처 변경입니다.

오늘날의 구현:

  • lastSourceUrl 필드는 현재 재생 중인 소스 URL을 추적합니다.
  • QueueNext는 현재 트랙과 큐에 추가된 트랙 모두에 대해 FilePropertyType.SampleRate/Channels/Kind를 읽고 불일치 시 NextUri:FormatChange를 기록합니다.

F17 - 탐색 후 진행률 표시줄 재동기화

무엇: Seek() 함수는 성공적인 Seek SOAP 후 이미 GetPlayPositionInformation()을 호출하여 "전혀 재동기화되지 않는" 경우를 수정했습니다. F17은 UPnP의 1초 RelTime 양자화로 인해 발생하는 최대 1초의 나머지 드리프트를 해결합니다. 장치에서 보고된 위치가 사용자가 요청한 대상의 1초 이내로 반올림될 때 플러그인은 이제 장치의 잘림 대신 사용자의 서브초 정확한 값을 신뢰합니다. 장치가 극적으로 다른 값(1초 이상 차이)을 보고할 때만 해당 값을 사용합니다(탐색이 요청한 위치와 다른 곳에 착륙한 경우, 예: 일부 코덱의 키프레임에 스냅).

왜: 이것이 없으면 장치의 "2:30" 보고서에 고정된 2:30.500으로 탐색하면 진행률 표시줄이 실제보다 약 500ms 뒤쳐져 표시됩니다. F17 이후에는 일반적인 트랙 내 스크럽의 경우 표시줄이 사용자의 의도와 일치하고, 키프레임에 스냅되는 예외의 경우에도 장치의 보고서를 존중합니다.


F18 - 연속 스트림 / NextURI 연동

무엇: 이제 두 가지 연동이 적용됩니다.

  1. 런타임: Settings.ContinuousOutput이 켜져 있을 때 QueueNextFalse를 조기 반환합니다. 연속 스트림은 자체적인 무음 메커니즘(하나의 긴 연결된 스트림)입니다. 그 위에 SetNextAVTransportURI를 보내면 각 트랙이 개별 URI인지 연속 흐름의 일부인지에 대해 장치를 혼란스럽게 합니다.
  2. UI: 사용자가 전역 연속 스트림 확인란을 선택하면 현재 표시된 프로필의 forceNativeStream이 자동으로 선택 해제됩니다. 연속 스트림은 항상 트랜스코딩되므로 강제 네이티브는 조합에서 무의미합니다.

왜: 사용자가 두 가지 충돌하는 무음 메커니즘을 동시에 활성화하는 것을 방지합니다. F18이 없으면 렌더러에 따라 정의되지 않은 동작으로 장치는 각 후속 트랙에 대해 연속 스트림 URI와 NextURI를 모두 수신할 것입니다.


F19 - 빈 NextURI 오류 무시

무엇: 빈 URL로 SetNextAVTransportURI가 호출될 때(예: 목록의 마지막 트랙), 일부 장치는 SOAP 오류를 반환합니다. F19는 이러한 오류를 자동으로 삼킵니다. 기록되지만 오류로 전파되지 않습니다.

왜: "다음 트랙 없음" 조건은 오류가 아니라 정상입니다. 이를 치명적인 것으로 취급하면 로그를 오염시키고(일부 흐름에서) 재시도 폭풍을 유발합니다.


MIME 유형 및 DLNA 메타데이터

F20 - MP3 MIME → audio/mpeg

무엇: 표준 준수 MP3 MIME 유형은 audio/mpeg이며, audio/mp3가 아닙니다. 후자는 대부분의 장치가 허용하는 일반적인 오칭이지만, 더 엄격한 렌더러는 이를 거부합니다.

왜: 표준을 따르는 더 엄격한 장치에서 재생을 자동으로 수정합니다. yaiol 코드베이스는 이미 이것을 올바르게 가지고 있었습니다. 변경할 필요가 없습니다.


F21 - MIME 유형 순서: 비-x- 변형 우선

무엇: 장치가 audio/flacaudio/x-flac을 모두 광고할 때 플러그인은 비-x- 변형을 먼저 반환합니다. 표준 및 실험적 MIME을 모두 가진 모든 코덱에 대해 동일합니다.

왜: x- 접두사는 실험적/비공식 MIME을 표시합니다. 일부 렌더러는 표준 형식에서 더 잘 작동합니다. 작은 재정렬, 실제 영향.


F22 - Opus MIME 유형 지원

무엇: Opus를 스트리밍 가능한 오디오 코덱으로 인식합니다. Opus 트랙을 제공할 때 audio/opus MIME을 보냅니다.

왜: Opus는 이제 일반적입니다(현대 음성/음악 절충 코덱). F22가 없으면 플러그인은 Opus 파일을 처리하는 장치에도 스트리밍을 거부할 것입니다.


F23 - Monkey Audio (APE) 소스 파일 지원

무엇: .ape 파일을 스트리밍/트랜스코딩을 위한 유효한 소스 코덱으로 인식합니다.

왜: APE는 틈새 시장이지만 충성도 높은 사용자 기반을 가진 무손실 형식입니다. 이를 추가하는 데 비용이 거의 들지 않으며 해당 사용자를 위해 라이브러리를 잠금 해제합니다.


F24 - AAC / ALAC MIME 대체

무엇: 장치가 AAC 또는 ALAC을 지원하지만 UPnP 서비스 설명에 명시적으로 광고하지 않는 경우, 플러그인은 어쨌든 대체로 제공합니다.

왜: AAC를 잘 처리하는 여러 장치가 기능 XML에 이를 나열하는 것을 잊었습니다. F24가 없으면 플러그인은 시도조차 하지 않고 트랜스코딩을 강제합니다. F24를 사용하면 플러그인은 시도하고 장치가 할 수 있다면 네이티브로 처리하도록 합니다.


F25 - 네이티브 + 인코딩된 WAV 스트림에 대한 DLNA 유형 플래그

무엇: DLNA 유형 플래그(LPCM, WAVE, MP3와 같은 프로필 식별자)는 장치가 수신하는 것과 일치해야 합니다. F25는 네이티브 스트림과 인코딩된 WAV 스트림이 올바르게 플래그 지정되도록 합니다.

왜: DLNA 유형 불일치는 일부 장치가 재생을 완전히 거부하거나 잘못된 디코더를 적용하게 합니다.


F26 - FLAC 파일에 대한 DLNA 헤더

무엇: FLAC 스트림은 헤더에 적절한 DLNA 프로필 식별자를 얻습니다.

왜: 이것이 없으면 FLAC을 지원하는 일부 장치는 스트림을 인식하지 못합니다.


F27 - 메타데이터의 비트레이트 계산 수정

무엇: 연속 스트림 res@bitrate(sampleRate * channels * bitsPerSample) / 1000으로 계산되었습니다. 이는 UPnP DIDL 사양에서 속성을 초당 바이트 수로 정의하는 것과 약 125배 차이가 나는 kbps였습니다. 이제 1000 대신 8로 나눕니다.

왜: 장치에 잘못된 비트레이트 표시. 대부분의 렌더러에서는 미미한 문제이지만, 일부는 값에서 스트림 버퍼를 할당하고 실제보다 약 125배 작은 스트림에서 끊김이 발생합니다. 비연속 소스 파일 경로는 이미 이것을 올바르게 가지고 있었습니다((bitrate_kbps * 1000) \ 8 = 초당 바이트). 연속 스트림 경로만 잘못되었습니다.


F28 - 메타데이터 시간 형식 수정 (Marantz)

무엇: DIDL의 res@durationH:MM:SS 형식(예: 0:03:42)이었습니다. UPnP DIDL 사양은 형식을 H+:MM:SS[.F+]로 정의합니다. 엄격하게는 소수 초가 선택 사항이지만 권장됩니다. 일부 Marantz 장치는 맨몸 형식을 유효하지 않은 것으로 취급하고 지속 시간 표시를 비워둡니다. 이제 H:MM:SS.fff 형식(예: 0:03:42.000)으로 지정됩니다.

왜: 특정 브랜드에 특정한 표시 문제. 소수 초가 있는 ISO 스타일 형식은 다른 장치에 영향을 주지 않고 이를 수정합니다. 두 DIDL 방출 지점(소스 파일 경로 + WriteAudioFileDIDL의 인코딩된 스트림 경로)에 적용되었습니다.

동일한 통과에서 보너스 수정: pv:addedTimepv:lastPlayedTime은 DateTime 형식 문자열에서 HH(24시간 시계) 대신 hh(12시간 시계)를 사용했습니다. 13:00에서 23:59 사이에 추가되거나 재생된 모든 트랙은 필드를 표시하는 장치에서 잘못된 시간(예: 17:42 → "05:42")으로 렌더링되었습니다. 이제 HH를 사용합니다.


F29 - 인코딩된 MP3 탐색 지원 (CBR)

무엇: 트랜스코딩된 MP3 스트림은 이제 DLNA.ORG_OP=10(바이트 전용) 대신 DLNA.ORG_OP=11(바이트 및 시간 탐색 모두)을 광고합니다. 이전에 트랜스코딩된 MP3에서 시간 탐색을 거부했던 장치는 이제 진행률 표시줄/탐색 UI를 정상적으로 구동할 수 있습니다.

왜: MusicBee의 트랜스코더는 HighQuality 사전 설정에서 고정 비트레이트 MP3를 생성하므로 바이트 ↔ 시간 매핑은 선형입니다. 장치는 인코더 측 지원 없이 시간 탐색 요청을 HTTP Range 바이트 탐색으로 자체적으로 변환할 수 있습니다. OP=11을 광고하면 장치에서 해당 UI가 잠금 해제됩니다. F29가 없으면 트랜스코딩된 MP3 내에서 탐색하는 사용자는 탐색이 자동으로 무시되거나 트랙 시작으로 이동되었습니다.

구현: ItemManager.vbGetEncodeFeature를 재구성하여 인라인 If를 읽기 쉬운 If/ElseIf/Else 체인으로 분리했습니다. MP3는 명시적으로 OP=11을 얻습니다. 다른 비-PCM 코덱은 OP=10을 유지합니다. AAC/FLAC 등에는 변경 사항이 없습니다. MusicBee가 보장하지 않는 CBR 여부에 대한 코덱별 확인이 필요할 것입니다.


F30 - .mpeg 파일 확장자 처리

무엇: .mpeg(및 더 희귀한 .mpe) 확장자를 가진 파일은 이제 GetCodec에서 FileCodec.Mp3로 인식됩니다. F30 이전에는 FileCodec.Unknown을 반환하고 라이브러리에서 자동으로 거부되거나 트랜스코딩 소스가 될 수 없었습니다.

왜: 오래된 MPEG-1 Layer 3 아카이브는 .mp3 대신 .mpeg를 사용하기도 했습니다(사양은 둘 다 허용). 30만 개 라이브러리에서 몇 개의 파일만 있어도 "MusicBee는 보여주지만 플러그인은 보여주지 않는다"는 느낌을 줄 수 있습니다. 사용자에게 혼란스럽습니다.


재생 동작

F31 - 라디오 스트림 자동 연속 모드 사용

무엇: WriteAudioFileDIDL은 이제 Library_GetFileProperty를 통해 소스 URL의 Kind 속성을 조사하고, Kind가 "Stream"으로 끝나는 모든 파일(MusicBee는 라디오에 대해 "MP3 Stream", "Internet Stream" 등을 보고함)을 전역 Settings.ContinuousOutput 토글에 관계없이 연속으로 처리합니다. 연속 스트림 DIDL 분기(제목: "Continuous Stream", id="continuousstream", 고정 PCM/Wave 출력)가 사용됩니다. 장치는 단일 무한 스타일 스트림을 봅니다.

왜: 라디오 스트림에는 트랙 경계, 고정 길이, 탐색이 없습니다. DIDL에서 이를 개별 파일처럼 취급하면 플러그인이 존재하지 않는 바이트 범위와 지속 시간을 광고하게 됩니다. MusicBee가 이미 "이것은 스트림입니다"라고 알려주었을 때 자동으로 전환하면 사용자가 생각할 필요 없는 위험을 제거합니다.

범위: MusicBee가 재생을 구동할 때만 적용됩니다(musicBeePlayToMode). 라이브러리 가져오기 경로(UPnP 클라이언트 탐색)는 변경되지 않습니다. 라디오 URL은 드물고 명시적인 테스트 없이는 사용자 대면 동작이 변경되어서는 안 됩니다.


F32 - 코덱 광고 대체

무엇: 장치가 특정 코덱을 광고하지 않거나 플러그인이 장치의 기능 XML을 구문 분석할 수 없는 경우, 플러그인은 스트림을 즉시 거부하지 않습니다. 대신, 스트림을 제공하려고 시도하고 장치가 결정하도록 합니다.

왜: 많은 장치가 불완전하거나 읽을 수 없는 기능 XML을 가지고 있지만 실제로는 코덱을 잘 처리합니다. F32는 명백한 거부 대신 작은 "최고의 추측 및 시도"를 교환합니다.


F33 - 진행률 표시줄 동기화 개선

무엇: 폴링 사이의 위치는 이미 단일 앵커(currentPlayStartTicks)에서 벽시계로 외삽되므로 진행률 표시줄은 서브초 속도로 부드럽게 업데이트됩니다. 남아있는 지터 원인은 새로 시작된 트랙의 초기 앵커였습니다. 이전 코드는 상태 타이머가 처음 재생 상태를 감지한 순간에 position=0이라고 가정했지만, 그때쯤 장치는 이미 100-500ms(하나의 폴링 간격) 동안 재생 중이었을 수 있습니다. MusicBee의 진행률 표시줄은 0에서 시작한 다음 실제가 따라잡을 때 앞으로 점프했습니다.

F33 수정: 새 트랙에서 처음으로 재생으로 전환할 때(currentPlayStartTimeEstimated=True), GetPlayPositionInformation()을 호출하여 장치의 실제 현재 위치를 가져온 다음, 이를 기준으로 앵커를 설정합니다. UPnP는 1초 해상도만 보고하므로 앵커는 여전히 양자화되지만, 0으로 가정하는 것보다 훨씬 진실에 가깝습니다.

왜: 특히 트랙 변경 직후에 더 부드럽고 정확한 진행률 표시. 1초 UPnP 보고 해상도 자체를 피할 방법은 없습니다. 이는 사양입니다.


F34 - 트랙 변경 후 진행률 표시줄 지터

무엇: 새 트랙에 대해 PlayToDevice가 호출될 때 플러그인은 SOAP-Play와 새 재생 상태를 감지하는 첫 번째 상태 타이머 폴링 사이의 ~100ms 창 동안 currentPlayPositionMscurrentPlayStartTicks를 이전 트랙 값으로 남겨두곤 했습니다. MusicBee의 진행률 표시줄은 이전 트랙의 을 잠시 표시한 다음 0으로 돌아가고 다시 올라갔습니다. F34는 PlayToDevice 진입 시(SOAP 작업이 시작되기 전에 트랙 변경이 발생하고 있음을 아는 순간) 둘 다 0으로 설정합니다.

왜: 빠른 건너뛰기 사용 사례(수동 다음 또는 무음 전환)에서 시각적 결함. 이제 MusicBee의 첫 번째 PlayPositionMs 쿼리 후 Play는 깨끗하게 0을 반환한 다음, F33의 GetPlayPositionInformation은 첫 번째 상태 변경 틱에서 장치의 실제 위치로 이를 개선합니다.

구현: PlayToDevice 상단에 네 줄, F33의 전환 시간 정확한 앵커링과 짝을 이룹니다.


F35 - "트랜스코딩 강제" 버그

무엇: 트랜스코딩 강제는 특정 조합에서 여전히 트랜스코딩을 건너뛸 수 있었습니다. F04 프로필별 재작업 후 두 가지 특정 간격이 봉인되었습니다.

  1. ForceNativeStream과의 우선순위. 둘 다 True일 때(스키마 마이그레이션 또는 부분 설정 파일을 통해 발생할 수 있음), ForceTranscoding이 이제 완전히 승리합니다(If streamingProfile.ForceTranscoding Then forceEncode = True ElseIf streamingProfile.ForceNativeStream Then forceEncode = False). UI 상호 배제는 사용자가 둘 다 선택하는 것을 방지하지만, 런타임 가드는 디스크에서 일관되지 않게 로드된 모든 상태를 처리합니다.
  2. bypassTranscodeDecision 논리. 이전: streamingProfile.ForceNativeStream AndAlso Not Settings.ForceTranscoding. 이제: streamingProfile.ForceNativeStream AndAlso Not streamingProfile.ForceTranscoding - 동일한 우선순위 규칙이지만 동일한 프로필별 범위에서.

왜: "강제"는 강제를 의미해야 합니다. 사용자가 장치에 대해 ForceTranscoding을 명시적으로 활성화한 경우, 다른 플래그가 어떻게 결합되든 플러그인은 자동으로 네이티브 스트리밍으로 넘어가지 않아야 합니다.


F36 - 렌더러 종료 예외

무엇: Plugin.ReceiveNotification을 최상위 Try/Catch로 래핑하여 MusicBee의 알림 펌프로 다시 전파하는 대신 포착되지 않은 예외를 기록합니다.

왜: MusicBee의 알림(PlayStateChanged, VolumeMuteChanged 등)은 SOAP를 통해 렌더러와 통신하는 ControlPointManager로 디스패치됩니다. 개별 호출 사이트는 이미 SOAP 호출 주변에 Try/Catch를 가지고 있었지만, 충분히 이상한 타이밍 사례(예: 동일한 알림 핸들러에서 두 SOAP 호출 사이에 렌더러가 죽는 경우)는 여전히 탈출할 수 있었습니다. 최상위 래퍼는 최종 안전망이므로 사용자는 MusicBee에서 일반적인 "TargetInvocationException" 팝업을 볼 일이 없습니다.

구현: 기존 본문을 ReceiveNotificationInternal로 이름을 바꾸고 Try { ReceiveNotificationInternal(...) } Catch { LogError(...) }를 수행하는 얇은 래퍼 ReceiveNotification을 추가했습니다. ControlPointManager 내부의 기존 메서드별 Try/Catch 인프라(모든 PostSoapRequest 호출 주변)는 유지됩니다. F36은 벨트 + 서스펜더입니다.


F37 - 긴 트랙 탐색 시 잘못된 전환 트리거

무엇: 긴 트랙 내에서 탐색하면 일부 렌더러에서 짧은 Stopped→Playing 주기가 발생할 수 있습니다. 구별 없이 ProcessNewPlayState.Stopped는 이를 트랙의 자연스러운 끝으로 취급하고 Player_PlayNextTrack을 호출하여 사용자가 스크럽하려고 했을 때 MusicBee를 진행시킵니다. F37은 Seek()에서 lastUserInitiatedSeek를 스탬프하고 Stopped 핸들러에 5초 가드를 추가합니다(기존 lastUserInitiatedStop 창을 미러링).

왜: 탐색 중 자동 다음 트랙 건너뛰기는 아무도 원인을 추측할 수 없는 버그 중 하나입니다. 사용자는 "이상하다, 앞으로 스크럽하려고 했는데 이제 다음 노래가 재생되고 있다"고 생각합니다. 수정은 기계적입니다. 이미 적용된 사용자 중지 구별과 동일한 패턴입니다.


F38 - 충돌 가능성이 있는 코덱에 대한 향상된 탐색 처리

무엇: MP3 탐색 시 BubbleUPnP 충돌이 일반적인 증상이었습니다. 감사 후, 현재 yaiol 탐색 코드는 이미 올바른 작업을 수행합니다. 네이티브 경로는 HTTP Range를 올바르게 처리하고(206, Content-Range, AcceptRanges), 인코딩된 경로는 X-AvailableSeekRange를 광고하고 들어오는 timeSeekRange.dlna.org / npt 헤더를 구문 분석하며, DLNA.ORG_OP 플래그는 실제 스트림 기능을 반영합니다(문제 Platinum 장치에 대한 DisablePcmTimeSeek 옵트아웃 포함). 현재 BubbleUPnP 4.6.4에서 사용자 테스트: 충돌 없음.

왜: BubbleUPnP MP3 탐색 충돌은 2024년경에 보고되었으며 앱은 그 이후로 약 16개월 동안 수정되었습니다. F29(인코딩된 MP3 OP=11)는 이를 다시 노출할 수 있는 새로운 변수였지만, 테스트된 버전에서는 그렇지 않습니다.

충돌이 다시 발생하면: 수정 형태는 플래그 지정된 코덱에 DLNA.ORG_OP=10(바이트 전용)을 강제하는 프로필별 "제한된 탐색" 토글이 될 것입니다. DisablePcmTimeSeek가 PCM에 대해 이미 작동하는 방식과 유사합니다. 그때 추가하고 미리 추가하지 않습니다.


UI 및 로깅

F39 - "추가" 버튼이 새 프로필을 선택합니다.

무엇: 장치 프로필 목록에서 "추가"를 클릭하면 새 프로필이 생성되고 자동으로 선택되므로 사용자가 즉시 필드를 편집할 수 있습니다. 섹션화된 대화 상자 리팩토링은 이미 이를 수행합니다. 직접 추가 경로와 템플릿에서 추가 경로 모두 Me.activeStreamingProfiles.SelectedIndex = Me.activeStreamingProfiles.Items.Count - 1로 끝납니다. 확인 결과 포크는 이미 이를 처리하고 있습니다. 변경할 필요가 없습니다.

왜: 작은 UX 문제였지만 여기서는 이미 문제가 아니었습니다.


F40 - 더 큰 최대 연결 + 경고 로그

무엇: 플러그인의 동시 스트림 제한(SemaphoreSlim around Sockets_Stream_File / Sockets_Encoder_Start)은 하드 코딩된 4였습니다. F40은 이를 일반 설정 페이지에서 사용자가 구성할 수 있도록 하고(기본값 16, 범위 1-256), 요청이 슬롯을 기다려야 할 때 MaxConnections 로그 줄을 추가하며, MusicBee 시작 이후 제한에 한 번 이상 도달한 경우 설정 대화 상자의 왼쪽 하단에 빨간색 ⚠ 최대 연결 배지를 표시합니다.

왜: 장치가 병렬 요청을 발생시킬 때(일부 Marantz/Linn의 아트워크 스캔 중, 활성 재생과 함께 BubbleUPnP의 메타데이터 조사), 추가 요청은 세마포어 뒤에서 자동으로 차단되었습니다. 사용자는 눈에 보이는 원인 없이 "장치가 느리다"고 보았습니다. 로그 줄은 기술 디버깅에 좋지만 비기술 사용자는 로그를 읽지 않습니다. 설정 대화 상자의 눈에 띄는 배지는 플러그인 환경설정을 여는 모든 사람이 제한 도달 조건을 발견할 수 있도록 합니다.

구현:

  • MusicBeeUpnp.vbWaitOnSendBarrier(logTag)에서 대기를 중앙 집중화했습니다. 두 호출 사이트(MediaServerDevice.GetFile, Encoder.StartEncode) 모두 이를 사용합니다.
  • Settings.MaxConnections는 설정 스키마 v8에 유지됩니다.
  • Plugin.MaxConnectionsHitWaitOnSendBarrier 내에서 설정되는 고정 세션 플래그입니다. MusicBee 다시 시작 시에만 재설정됩니다.
  • SettingsDialog.maxConnectionsBadge(16, 410)에 있는 빨간색 굵은 레이블로, Plugin.MaxConnectionsHit가 True일 때만 표시됩니다. 원인과 해결책을 설명하는 도구 설명이 있습니다.
  • 세마포어는 유형 로드 시 한 번 초기화되므로 설정을 변경하려면 MusicBee 다시 시작이 필요합니다(필드 레이블에 명시됨).

F41 - "ReplayGain/DSP로 인한 인코딩" 로그

무엇: 별도의 "RG용 인코딩" / "DSP용 인코딩" 로그 줄 대신, F42의 단일 StreamDecision 줄은 MB-DSP/EQ, MB-ReplayGain, Profile-DSP/EQ, Profile-ReplayGain을 누적된 이유로 포함합니다. 동일한 진단 값, 더 적은 노이즈.

왜: 사용자는 주어진 트랙에 대해 트랜스코딩이 발생하는 모든 이유를 한 로그 줄에서 볼 수 있으며, 흩어져 있지 않습니다. 자세한 내용은 F42를 참조하십시오.


F42 - "렌더러가 소스 코덱을 지원하지 않음" 로그

무엇: 재생 대상 트랙당 "네이티브 CODEC" 또는 "트랜스코딩 CODEC→CODEC reason=…"을 나타내는 StreamDecision 로그 줄을 추가했습니다. 이유 필드는 트랜스코딩을 트리거한 모든 조건을 누적합니다. MB-DSP/EQ, MB-ReplayGain, Profile-DSP/EQ, Profile-ReplayGain, WebFile, VirtualFile, ForceTranscoding(global), SampleRate<min/SampleRate>max, DownmixToStereo, DeviceLacksCodec(X), BandwidthConstrained.

왜: 사용자는 네이티브로 스트리밍될 것으로 예상했던 파일에서 예상치 못한 CPU 스파이크로 인해 혼란스러워했습니다. 트랙당 한 줄의 로그는 트랜스코딩을 유발한 정확한 조건을 알려줍니다. 필드에 DeviceLacksCodec(Flac)이 표시되면 장치의 프로토콜 정보가 불완전했으며 F32의 대체가 시작되기를 원할 수 있음을 즉시 알 수 있습니다.

구현: 결정 체인을 통해 점진적으로 구축되는 단일 누적 문자열; 끝에서 한 번 기록됩니다. 프로덕션에서 로그 노이즈를 피하기 위해 Settings.LogDebugInfo에 의해 제어됩니다.


F43 - SetNextAVTransport 로그에 소스 URL 표시

무엇: QueueNext 로그 항목에는 이제 stream=<HTTP 스트리밍 URL>과 함께 source=<MusicBee 라이브러리 경로>가 포함됩니다. 성공 경로와 실패 경로(QueueNext:Failed)에도 동일한 변경 사항이 적용되었습니다.

왜: 큐에 추가된 트랙 문제를 디버깅할 때 스트리밍 URL(/encode/aabbccdd0.flac)은 자체적으로 불투명합니다. 모든 트랙에 대해 동일합니다. 소스 URL은 MusicBee가 큐에 추가하려고 시도한 정확한 파일을 알려주는 사람이 grep할 수 있는 라이브러리 경로입니다.


F44 - 더 나은 MIME 유형 오류 로깅

무엇: Activate 중 두 가지 새로운 로그 항목:

  • Activate:MimeUnverified - 장치의 GetProtocolInfo 응답에서 잘못된 형식의 항목당 발생하며, 구문 분석할 수 없는 항목을 명명합니다(따라서 사용자는 예를 들어 "Marantz가 일부 코덱에 대해 http-get:*::*를 반환했습니다. 기능이 확인되지 않았습니다. F32의 대체가 추측할 것입니다"와 같은 내용을 볼 수 있습니다).
  • Activate:NoSinkInfo - 장치가 <Sink> 요소를 전혀 반환하지 않은 경우 한 번 발생합니다. SupportedMimeTypes가 Nothing으로 유지되고 IsCodecSupported가 "모든 것이 작동한다고 가정"으로 저하됨을 의미합니다. 나중에 "장치가 스트림을 거부했습니다" 오류가 나타날 때 유용한 컨텍스트입니다.

왜: F44 이전에는 이러한 자동 기능 대체로 인해 사용자는 트랙이 예상과 달리 트랜스코딩되거나 장치에서 거부되는 이유를 추측해야 했습니다. 이제 Activate:에 대한 단일 grep은 장치의 기능 정보가 사용 가능한지 여부를 보여줍니다.


F45 - 더 나은 메타데이터 오류 로깅

무엇: ContentDirectoryService.vbBrowse 예외 로그는 이전 yaiol 작업(Alia Vox 버그 세션)에서 ObjectID 및 스택 추적으로 이미 풍부해졌습니다. F45는 BrowseFlag(메타데이터 vs 자식), Filter(클라이언트가 요청한 속성), sortCriteria, partialResultLength(실패 전에 생성된 DIDL의 바이트 수 - 잘못된 트랙이 배치에서 얼마나 멀리 떨어져 있는지 나타냄)로 이를 더욱 확장합니다.

왜: DIDL 중간에 문제가 발생하면 partial-length 값은 실패가 배치의 첫 번째 트랙에서 발생했는지(partial=0) 아니면 중간에 발생했는지(partial=N)를 알려줍니다. 배치의 startingIndex와 결합하여 문제가 있는 트랙 인덱스를 식별할 수 있습니다. Filter 및 BrowseFlag는 클라이언트가 원하는 탐색 유형을 설명합니다. 때로는 동일한 ID에 대한 메타데이터 전용 탐색이 자식 탐색이 성공하는 곳에서 실패하기도 합니다.


네트워킹

F46 - 자동 모드는 실제 네트워크 어댑터에서만 알림

무엇: 인터페이스 자동 모드에서 플러그인은 이전에 작동 중인 모든 IPv4 어댑터에서 자신을 알렸습니다(SSDP). VPN 터널(NordLynx)이나 가상 스위치(Hyper-V / WSL / Docker)도 실행 중인 컴퓨터에서는 동일한 라이브러리가 이들 어댑터에서도 알려졌기 때문에, 캐스팅하는 제어 지점이 서버를 두세 번 발견하고 라이브러리를 중복 복사본으로 나열했습니다. 자동 모드는 이제 실제 IPv4 기본 게이트웨이가 있는 어댑터(HasIPv4Gateway)만 유지합니다 - 터널 및 가상 스위치 어댑터에는 게이트웨이가 없으므로 알림 목록에서 제외됩니다. 사용자가 고정한 주소는 여전히 무조건 우선하며(해당 인터페이스에서만 알림), 게이트웨이를 보고하는 어댑터가 하나도 없으면 선택기가 모든 어댑터로 폴백하므로 알려지는 주소 목록이 비는 일은 없고 플러그인이 보이지 않게 되는 일도 없습니다.

왜: 중복은 "VPN을 사용한다"라서 생기는 것이 아니라, LAN 어댑터와 터널/가상 어댑터에서 동시에 알리기 때문에 생깁니다. 그래서 하나의 제어 지점이 같은 서버를 두 주소에서 보게 됩니다. 소비자용 VPN(NordVPN/NordLynx)은 인터넷으로 향하는 트래픽만 터널링합니다. DLNA 렌더러는 LAN에 있고 로컬 서브넷 트래픽은 터널을 우회하므로, 터널 어댑터는 어차피 렌더러에 도달하지 못합니다 - 이를 제외하면 유령 복사본만 사라질 뿐, 작동하는 경로가 사라지는 일은 없습니다. 게이트웨이 검사는 실제 LAN/Wi-Fi 어댑터를 터널이나 가상 스위치와 구분하는 저렴하고 신뢰할 수 있는 신호입니다. N05(이런 링크에서 알림이 어떻게 전송되는지 수정 - 브로드캐스트 대신 멀티캐스트)를 보완하며, F46은 애초에 어떤 어댑터에서 알릴지를 결정합니다.

알려진 제한: 렌더러가 정말로 터널 건너편에 있는 메시/원격 액세스 VPN(Tailscale, ZeroTier, 집으로 연결하는 WireGuard)은 보통 기본 게이트웨이가 없는 어댑터로 나타나므로 자동 모드가 이것도 제외합니다. 이런 사용자는 대신 VPN 주소를 고정하면 되며, 고정 주소는 게이트웨이 필터보다 우선합니다.