문서 Picture-in-Picture 명세

커뮤니티 그룹 초안 보고서,

이번 버전:
https://wicg.github.io/document-picture-in-picture/
이슈 추적:
GitHub
명세 내 인라인
편집자:
(Google Inc.)

개요

이 명세는 웹 개발자가 항상 최상위에 표시되는 창에 HTMLDocument를 채울 수 있도록 합니다.

문서의 상태

이 명세는 웹 플랫폼 인큐베이터 커뮤니티 그룹에 의해 발행되었습니다. W3C 표준이 아니며 W3C 표준 트랙에 포함되어 있지 않습니다. W3C 커뮤니티 기여자 라이선스 계약(CLA)에 따라 제한적 옵트아웃 및 기타 조건이 적용됩니다. W3C 커뮤니티 및 비즈니스 그룹에 대해 자세히 알아보세요.

1. 소개

이 섹션은 표준이 아닙니다.

현재 HTMLVideoElement를 Picture-in-Picture 창(requestPictureInPicture())에 넣는 웹 API가 존재합니다. 이로 인해 웹사이트가 맞춤형 picture-in-picture 경험(PiP)을 제공하는 데 제한이 있습니다. 우리는 항상 최상위에 표시되는 창에서 웹사이트가 전체 Document를 제공할 수 있도록 이 기능을 확장하고자 합니다.

이 새로운 창은 기존 open() 메서드를 통해 동일 출처로 빈 창을 여는 것과 비슷하지만 몇 가지 차이점이 있습니다:

2. 의존성

이 명세의 IDL 조각은 Web IDL 명세에 명시된 대로, 규격을 준수하는 IDL 조각으로 해석해야 합니다. [WEBIDL]

3. 보안 고려사항

3.1. 보안 컨텍스트

이 API는 [SECURE-CONTEXTS]에 한정됩니다.

3.2. 스푸핑

사용자 에이전트는 DocumentPictureInPicture 창에서 악의적인 웹사이트가 다른 창 위에 띄우는 기능을 악용하여 다른 웹사이트나 시스템 UI를 스푸핑하는 것을 막기 위해 충분한 UI를 제공해야 합니다.

3.2.1. 위치 지정

사용자 에이전트는 웹사이트가 창의 위치를 설정하지 못하도록 해야 하며, 이는 사용자가 창을 다른 페이지의 UI 일부로 오인할 수 있는 위치를 일부러 지정할 수 없게 하기 위함입니다. 특히 moveTo()moveBy() API는 document picture-in-picture 창에서 비활성화되어야 합니다.

3.2.2. 출처 가시성

사용자 에이전트는 DocumentPictureInPicture 창을 제어하는 출처를 사용자에게 항상 명확히 보여주어야 하며, 사용자가 콘텐츠가 어디서 오는지 인식할 수 있어야 합니다. 예를 들면, 사용자 에이전트는 창의 제목 표시줄에 웹사이트의 출처를 표시할 수 있습니다.

3.2.3. 최대 크기

사용자 에이전트는 항상 최상위에 표시되는 창으로 화면 전체를 덮거나 PiP 창에 사용자가 갇히는 상황, 그리고 사용자의 데스크탑이 스푸핑되는 것을 막기 위해 document picture-in-picture 창의 최대 크기를 제한해야 합니다.

3.2.4. 전체화면

사용자 에이전트는 문서 PiP(Picture-in-Picture) 창이 전체화면으로 전환되지 않도록 막아야 하며, 그 근거는 최대 크기 제한에 대해 언급한 논리와 같다. 즉, requestFullscreen() API가 문서의 PiP 창에 있는 모든 요소에서 비활성화되어야 함을 의미한다.

3.3. IFrame

이 API는 최상위 이동 가능 객체에서만 사용할 수 있습니다. 그러나 DocumentPictureInPicture Window 자체에는 HTMLIFrameElement가 포함될 수 있으며, 교차 출처 HTMLIFrameElement도 포함될 수 있습니다.

4. 개인정보 보호 고려사항

4.1. 지문 채취

PiP 창을 닫았다가 다시 열 때 이전 PiP 창의 크기와 위치를 재사용하면 사용자 경험이 더 원활해질 수 있습니다. 그러나 사용자 에이전트는 서로 다른 출처에 대해 크기/위치 정보를 재사용하지 않는 것이 좋습니다. 이것이 사용자의 지문 채취(식별)에 악용될 수 있기 때문입니다.

5. API

[Exposed=Window]
partial interface Window {
  [SameObject, SecureContext] readonly attribute DocumentPictureInPicture
    documentPictureInPicture;
};

[Exposed=Window, SecureContext]
interface DocumentPictureInPicture : EventTarget {
  [NewObject] Promise<Window> requestWindow(
    optional DocumentPictureInPictureOptions options = {});
  readonly attribute Window window;
  attribute EventHandler onenter;
};

dictionary DocumentPictureInPictureOptions {
  [EnforceRange] unsigned long long width = 0;
  [EnforceRange] unsigned long long height = 0;
  boolean disallowReturnToOpener = false;
  boolean preferInitialWindowPlacement = false;
};

[Exposed=Window, SecureContext]
interface DocumentPictureInPictureEvent : Event {
  constructor(DOMString type, DocumentPictureInPictureEventInit eventInitDict);
  [SameObject] readonly attribute Window window;
};

dictionary DocumentPictureInPictureEventInit : EventInit {
  required Window window;
};
DocumentPictureInPicture 객체는 웹사이트가 항상 위에 표시되는 새로운 Window 를 생성·열 수 있게 하며, 해당 Window 의 열림/닫힘과 관련된 이벤트를 들을 수도 있다.

Window 객체는 연관된 documentPictureInPicture API를 가진다. 이는 DocumentPictureInPicture 인스턴스로 해당 Window 와 함께 생성된다.

documentPictureInPicture getter의 단계는:
  1. thisdocumentPictureInPicture API를 반환한다.

DocumentPictureInPicture 객체는 연관된 마지막으로 연 창(last-opened window)을 갖는다. 이는 Window 객체로, 기본값은 null이고  requestWindow() 메서드 단계에서 지정된다.

window getter의 단계는:
  1. winthis마지막으로 연 창으로 한다.

  2. 만약 winnull이 아니고 winclosed 속성이 false라면, win을 반환한다.

  3. null을 반환한다.

requestWindow(options) 메서드 단계:
  1. Document Picture-in-Picture 지원false이면, "NotSupportedError" DOMException을 발생(throw)시킨다.

  2. thisrelevant global objectnavigable최상위(top-level) traversable이 아니라면, "NotAllowedError" DOMException을 throw한다.

  3. thisrelevant global objectnavigableIs Document Picture-in-Picture 불린값이 true이면 "NotAllowedError" DOMException을 throw한다.

  4. thisrelevant global object임시 활성화(transient activation)를 가지고 있지 않으면, "NotAllowedError" DOMException을 throw한다.

  5. options["width"] 가 존재 && 0 초과이고, options["height"] 가 없거나 0이면 RangeError를 throw한다.

  6. options["height"] 가 존재 && 0 초과이고, options["width"] 가 없거나 0이면 RangeError를 throw한다.

  7. 사용자 활성화 소모(Consume user activation)thisrelevant global object로 전달해 실행한다.

  8. winthis마지막으로 연 창으로 한다. winnull이 아니고 winclosed 속성이 false라면, close winnavigable을 실행한다.

  9. 선택적으로 user agent는 기존 PiP 창을 모두 닫을 수 있다.

  10. pip traversable새 top-level traversable 생성 결과(인자: thisrelevant global objectnavigableactive browsing context, "_blank")로 구한다.

생성된 DocumentURL은 `about:blank`이지만, document base URLrequestWindow() 를 호출한 호출자의 값으로 폴백된다. 일부 브라우저는 일반 `about:blank` 팝업에 이 폴백을 구현하지 않으니, whatwg/html#421 논의를 참고할 것. 구현자는 문서 PiP 창에서는 반드시 이 상속이 이뤄지도록 구현하여 상호운용성 문제를 방지해야 한다.

  1. pip traversableactive documentmodethisrelevant global object연결된 Documentmode 값으로 설정한다.

  2. pip traversableIs Document Picture-in-Picture 불린값을 true로 설정한다.

  3. options["width"]가 존재, 0 초과이면:

    1. 필요하다면 options["width"] 값이 너무 크거나 작으면 윈도우 사이즈가 사용자 친화적이도록 보정(clamp)하거나 무시할 수 있다.

    2. 필요하다면 pip traversableactive browsing context의 window의 뷰포트 좌우 픽셀 거리를 options["width"] 픽셀로 설정할 수 있다.

  4. options["height"]가 존재, 0 초과이면:

    1. 필요하다면 options["height"] 값이 너무 크거나 작으면 윈도우 사이즈가 사용자 친화적이도록 보정(clamp)하거나 무시할 수 있다.

    2. 필요하다면 pip traversableactive browsing context의 window의 뷰포트 상하 픽셀 거리를 options["height"] 픽셀로 설정할 수 있다.

options["preferInitialWindowPlacement"] 가 존재하고 true라면, user agent는 PiP 창의 이전 위치/크기를 무시하고 13, 14단계와 유사한 동작을 더 우선적으로 적용할 수 있다.

  1. options["disallowReturnToOpener"] 가 존재하고 true면, user agent는 PiP 창에서 opener 창으로 돌아갈 수 있는 UI 수단을 표시하지 않는다.

비디오 및 문서 PiP 모두에서 user agent는 원래 페이지로 돌아가면서 PiP 창을 닫는 버튼을 자주 표시한다. 대부분의 경우(특히 메인 문서로 영상을 돌려보내는 비디오 PiP 창)의 동작은 합당하지만, 문서 PiP에는 항상 적합하지 않을 수 있다. disallowReturnToOpener 는 웹사이트가 해당 문서 PiP 경험에 이를 허용할지의 힌트를 user agent에 제공한다.

  1. pip traversableactive browsing context의 window를 다른 창 위에 뜨도록(float) 설정한다.

  2. this마지막으로 연 창pip traversableactive window로 설정한다.

  3. 글로벌 작업을 큐에 넣는다(queue a global task). DOM 조작 작업 소스thisrelevant global object를 지정해 이벤트(enter)를 발생시킴(fire) (이벤트 타입: enter, DocumentPictureInPictureEvent, this에서, window 속성은 pip traversableactive window로 초기화한다).

  4. 이 Promise(프로미스)pip traversableactive window로 resolve해서 반환한다.

창의 크기는 웹사이트에서 지정 가능하지만, 초기 위치는 user agent에 달려 있다.

enter

PiP 창이 열렸을 때 DocumentPictureInPicture 에 발생(fire)된다.

6. 개념

6.1. 문서 Picture-in-Picture 지원

각 사용자 에이전트는 문서 Picture-in-Picture 지원 불린 값을 가지며, 그 값은 구현에 따라 다름(사용자 설정에 따라 달라질 수 있음)입니다.

6.2. DocumentPictureInPicture 창

최상위(traversable)문서 PiP 여부(Is Document Picture-in-Picture) 불린값을 가진다. 이 값의 기본값은 false이며, requestWindow() 단계에서 true로 설정될 수 있다.

사용자 에이전트는 일반적으로 최소화되거나 사용자에게 보이지 않는 창의 렌더링을 일시 중지하거나 스크립트 실행을 제한한다(throttle). 문서 PiP 창이 열려 있을 때 오프너(opener) 창이 문서 PiP 창의 콘텐츠에 영향을 주는 스크립트를 실행할 수 있다.

구현자에게는 이런 오프너 창의 쓰로틀링이 미칠 영향을 반드시 검토할 것을 권장하며, 개발자에게는 애플리케이션 로직을 PiP 창 내부에서 실행할 것을 권장한다.

6.3. 문서 Picture-in-Picture 창 닫기

이것을 definitely close 항목에 컨센서스가 충분해지면 합치세요.

definitely close 의 2단계를 다음과 같이 수정하세요. "If the result of checking if unloading is user-canceled for toUnload is not "continue", then return." →

  1. traversable문서 PiP 여부(Is Document Picture-in-Picture) 불린값이 true라면, 이 단계를 건너뛴다. 그렇지 않으면 unloading이 사용자에 의해 취소됐는지 검사 결과가 "continue"가 아니면 반환한다.

6.4. 기존 PiP 창 닫기

기존 picture-in-picture 창 닫기 방법:

  1. 사용자 에이전트의 최상위 이동 가능 객체 집합의 각 top-level traversable에 대해:

    1. top-level traversable문서 Picture-in-Picture 여부true라면 close top-level traversable를 실행한다.

    2. top-level traversable활성 문서pictureInPictureElement 값이 null이 아니면 PiP 종료 알고리즘top-level traversable활성 문서로 실행한다.

    3. top-level traversable활성 문서하위 이동 가능 객체들 각각에 대해:

      1. navigable활성 문서pictureInPictureElement 값이 null이 아니면 PiP 종료 알고리즘navigable활성 문서로 실행한다.

6.5. 단일 PiP 창

어떤 최상위 이동 가능 객체도 한 번에 하나의 문서 picture-in-picture 창만 열어야 합니다. 최상위 이동 가능 객체활성 창documentPictureInPicture APIlast-opened windownull이 아닐 때 또 다른 문서 picture-in-picture 창을 열려고 할 경우, 사용자 에이전트는 기존 last-opened windowrequestWindow() 단계에 따라서 닫아야 합니다.

하지만, 모든 최상위 이동 가능 객체에 대해 PiP 창을 하나만 허용할지는 구현과 플랫폼에 따라 다릅니다. 따라서 Picture-in-Picture 요청이 있을 때, 최상위 이동 가능 객체문서 Picture-in-Picture 여부true이거나 활성 문서pictureInPictureElementnull이 아닐 때의 동작은 구현에 맡깁니다: 사용자 에이전트는 기존 picture-in-picture 창 닫기를 할 수도 있고, 여러 PiP 창을 생성할 수도 있습니다.

6.6. 원본 또는 PiP 문서가 파기될 때 PiP 창 닫기

연관된 문서 Picture-in-Picture 창 닫기 방법 (주어진 Document document):

  1. navigabledocumentnode navigable로 한다.

  2. navigable최상위 이동 가능 객체가 아니면 이 단계를 중단한다.

  3. navigable문서 Picture-in-Picture 여부true이면 close navigable를 실행하고, 단계를 중단한다.

  4. winnavigable활성 창documentPictureInPicture APIlast-opened window로 한다.

  5. winnull이 아니고 winclosed 속성이 false이면 close winnavigable를 실행한다.

합의가 충분하면 destroy에 병합하십시오.

destroy의 끝에 10단계 추가:

  1. 연관된 문서 Picture-in-Picture 창 닫기document로 실행한다.

이 단계는 열린 문서 Picture-in-Picture 창이 있는 페이지가 닫히면 PiP 창도 함께 닫히도록 보장합니다.

6.7. 원본 또는 PiP 문서가 탐색될 때 PiP 창 닫기

이것을 navigate 항목에 컨센서스가 충분해지면 합치세요.

navigate 의 23.3단계를 다음과 같이 수정하고, 그 바로 뒤에 23.4단계를 삽입하세요:

  1. 글로벌 작업을 큐에 넣는다(Queue a global task) navigation and traversal task source 를 사용하여 navigableactive window에서 navigableactive document연관된 모든 Document Picture-in-Picture 창을 닫기navigableactive document로 전달하여 문서 및 하위 문서 중단(abort a document and its descendants) 를 실행한다.

  2. navigable최상위 traversable이면서 문서 PiP 여부(Is Document Picture-in-Picture) 불린값이 true이면, 이 단계를 중단(abort these steps)한다.

이 변경은 Document Picture-in-Picture 창이 열린 상태에서 페이지를 탐색하면 PiP 창도 함께 닫히도록 보장한다. 또한, Document Picture-in-Picture 창 자체의 문서를 탐색할 경우에도 PiP 창이 닫히도록 한다.

6.8. PiP 창 크기 조정

문서 picture-in-picture 창을 프로그래밍적으로 크기 조정하는 것은 유용하지만, 항상 최상위로 띄우는 성격상 윈도우 크기를 너무 자유롭게 조정하면 사용자에게 불편하거나 방해가 되는 식으로 남용될 수 있습니다. 이런 문제를 완전히 막지 않으면서도 window resize API의 활용을 제한하려면 document picture-in-picture 창에서는 해당 API가 사용자 제스처를 소모하도록 해야 합니다.

합의가 충분할 때 resizeTo() 에 병합하십시오.

resizeTo() 3단계 이후에 다음 단계를 추가하세요. "만약 target이 사용자 동작이 아닌 스크립트로 생성된 보조 브라우징 컨텍스트가 아니라면 반환한다.":

  1. target최상위 이동 가능 객체문서 Picture-in-Picture 여부true라면:

    1. thisrelevant global object순간 활성화를 갖지 않으면 "NotAllowedError" DOMException을 throw함.

    2. 사용자 활성화 소모thisrelevant global object에 실행.

합의가 충분하면 resizeBy() 에 병합하십시오.

3단계 이후에 resizeBy() 에 다음 단계를 추가하세요. "만약 target이 사용자 동작이 아닌 스크립트로 생성된 보조 브라우징 컨텍스트가 아니라면 반환한다.":

  1. target최상위 이동 가능 객체문서 Picture-in-Picture 여부true라면:

    1. thisrelevant global object순간 활성화를 갖지 않으면 "NotAllowedError" DOMException을 throw함.

    2. 사용자 활성화 소모thisrelevant global object에 실행.

6.9. PiP 창 이동하기

§ 3.2.1 위치 지정(Positioning)에서 설명한 스푸핑(spoofing) 방지를 위해, 문서 PiP(window)의 moveTo()moveBy() API를 비활성화(disable)합니다.

컨센서스가 충분해지면 이 내용을 moveTo()moveBy() 단계에 통합하세요.

moveTo() 의 3단계 이후에 다음 단계를 추가하세요: "만약 target이 스크립트에 의해 생성된(사용자 액션이 아닌) 보조 브라우징 컨텍스트(auxiliary browsing context)가 아니라면 return."

  1. 만약 target최상위(traversable)문서 PiP 여부 불린값이 true라면, return.

moveBy() 의 3단계 이후에 다음 단계를 추가하세요: "만약 target이 스크립트에 의해 생성된(사용자 액션이 아닌) 보조 브라우징 컨텍스트(auxiliary browsing context)가 아니라면 return."

  1. 만약 target최상위(traversable)문서 PiP 여부 불린값이 true라면, return.

6.10. 오프너 창 포커싱

PiP 창이 오프너 탭에 다시 포커스를 맞출 수 있다면(예: PiP 창의 크기가 작아서 사용자의 경험에 맞지 않는 경우 등) 유용할 수 있습니다. focus() API를 수정하여 PiP 창에서 오프너를 포커싱할 때 시스템 레벨 포커스를 받을 수 있게 허용합니다.

컨센서스가 충분해지면 이 내용을 focus() 에 통합하세요.

focus() 의 4단계 이후에 다음 단계를 추가하세요: "포커싱 단계(focusing steps)current로 실행."

  1. 만약 current최상위 traversable이라면:

    1. pipWindowcurrentactive windowdocumentPictureInPicture APIlast-opened window로 한다.

    2. 만약 pipWindownull이 아니고 pipWindowrelevant global objecttransient activation을 가지고 있다면:

      1. 사용자 활성화 소모pipWindowrelevant global object로 실행한다.

      2. current시스템 포커스(system focus)를 부여한다.

오프너에 시스템 포커스를 주는 것이 반드시 문서 picture-in-picture 창을 닫아야 함을 의미하지는 않습니다. 만약 포커싱 후에 문서 PiP 창을 닫길 원한다면 웹사이트는 PiP 창에서 close() 를 호출해서 닫을 수 있습니다.

6.11. CSS display-mode

CSS display mode 미디어 특성 picture-in-picture를 사용하면 웹 개발자는 웹앱의 일부 또는 전체가 PiP 모드로 표시될 때만 적용되는 특정 CSS 규칙을 작성할 수 있습니다.

6.12. 사용자 활성화 전파

문서 PiP 창의 특성상, 창 내부 버튼의 이벤트 핸들러가 실제로는 오프너 컨텍스트에서 실행되는 경우가 많습니다. 이는 해당 창이 activation consuming API 를 호출하는 것을 불편하게 만드는데, 때때로 PiP 창에는 transient activation이 있지만 오프너에는 없기 때문입니다.

이 문제를 쉽게 하기 위해, activation notification 단계에, 문서 PiP 창에서 사용자 활성화를 발생시킬 때 오프너에서도 사용자 활성화를 트리거하도록 업데이트합니다. 추가로, 오프너에서 사용자 활성화가 트리거될 때 문서 PiP 창 내의 동일 출처 프레임이 하위 프레임 전파와 유사하게 활성화되도록 합니다.

컨센서스가 충분해지면 이 내용을 activation notification 단계에 통합하세요.

activation notification 4단계 이후에 다음 3단계를 추가하세요: "Extend windows with the active window of each of document’s descendant navigables, filtered to include only those navigables whose active document’s origin is 같은 출처(same-origin)인 경우":

  1. 만약 documentnode navigable최상위(traversable)문서 PiP 여부 불린값이 true라면, Extend windowsdocumentnode navigable최상위(traversable)active browsing contextopener browsing contextactive window를 추가한다.

  2. document picture-in-picture windowdocumentnode navigable최상위(traversable)active windowdocumentPictureInPicture APIlast-opened window로 둔다.

  3. 만약 document picture-in-picture windownull이 아니라면, extend windowsdocument picture-in-picture window연관된 문서(associated document)하위 내비게이션(descendant navigables) 각각의 active window를 추가한다. 단, 각 navigableactive documentorigindocument picture-in-picture window연관된 문서origin동일 출처(same origin)인 것만 포함한다.

또, 활성화가 제대로 소모되어(opener에서 한 번, PiP 창에서 한 번 두 번 실행되지 않도록) 해야 합니다. 이를 위해 consume user activation 단계에 다음 동작을 추가합니다: PiP 창의 활성화 소모 시 오프너에서도, 오프너 활성화 소모 시 연관된 PiP 창에서도 소모가 일어납니다.

컨센서스가 충분해지면 이 내용을 consume user activation에 통합하세요.

consume user activation 의 3단계 이후에 다음 3단계를 추가하세요: "Let navigables be the inclusive descendant navigables of top’s active document."

  1. 만약 top문서 PiP 여부 불린값이 true라면, Extend navigablestopactive browsing contextopener browsing contextactive document포함된 하위 navigables를 추가한다.

  2. document picture-in-picture windowtopactive windowdocumentPictureInPicture APIlast-opened window로 둔다.

  3. 만약 document picture-in-picture windownull이 아니라면, Extend navigablesdocument picture-in-picture window연결된 문서(associated document)포함된 하위 navigables를 추가한다.

6.13. PiP 창 전체화면

§ 3.2.4 전체화면(Fullscreen)에서 언급했듯, 문서 PiP 창은 전체화면 진입이 허용되지 않습니다. 이 제약을 강제하기 위해 fullscreen is supported 정의를 업데이트합니다.

컨센서스가 충분해지면 이 내용을 fullscreen is supported 에 통합하세요.

fullscreen is supported의 "if there is no previously-established user preference, security risk, or platform limitation." 부분을 다음으로 교체하세요.

fullscreen is supportedWindow window가 아래 모든 조건을 만족할 때 true입니다:

fullscreen is supported를 호출하는 경우 반드시 Window 를 명시적으로 전달해야 합니다.

7. 예시

이 섹션은 규범적이지 않습니다

7.1. 비디오 플레이어를 PiP로 추출하기

7.1.1. HTML

<body>
  <div id="player-container">
    <div id="player">
      <video id="video" src="foo.webm"></video>
      <!-- 여기에 추가적인 플레이어 요소. -->
    </div>
  </div>
  <input type="button" onclick="enterPiP();" value="PiP 진입" />
</body>

7.1.2. JavaScript

// picture-in-picture 창을 가리키는 핸들.
let pipWindow = null;

function enterPiP() {
  const player = document.querySelector('#player');

  // 창을 비디오에 맞는 크기로 지정.
  const pipOptions = {
    width: player.clientWidth,
    height: player.clientHeight,
  };

  documentPictureInPicture.requestWindow(pipOptions).then((pipWin) => {pipWindow = pipWin;

    // 플레이어가 PiP로 이동했음을 암시하는 스타일 적용.
    playerContainer.classList.add('pip-mode');

    // 플레이어를 PiP 창으로 옮김.
    pipWindow.document.body.append(player);

    // PiP 종료 이벤트를 듣고 비디오 복구.
    pipWindow.addEventListener('pagehide', onLeavePiP.bind(pipWindow), { once: true });
  });
}

// PiP 창이 닫힐 때 호출됨.
function onLeavePiP() {
  if (this !== pipWindow) {
    return;
  }

  // 컨테이너에서 PiP 스타일 해제.
  const playerContainer = document.querySelector('#player-container');
  playerContainer.classList.remove('pip-mode');

  // 플레이어를 본 창에 다시 추가.
  const player = pipWindow.document.querySelector('#player');
  playerContainer.append(player);

  pipWindow = null;
}

7.2. PiP 창의 요소 접근하기

const video = pipWindow.document.querySelector('#video');
video.loop = true;

7.3. PiP 창에서 이벤트 리스닝

더 개선된 picture-in-picture 경험을 위해 사이트에서는 버튼 등 사용자 입력에 반응해야 하는 컨트롤을 직접 정의하고 싶을 때가 많습니다.

const pipDocument = pipWindow.document;
const video = pipDocument.querySelector('#video');
const muteButton = pipDocument.document.createElement('button');
muteButton.textContent = '음소거 토글';
muteButton.addEventListener('click', () => {
  video.muted = !video.muted;
});
pipDocument.body.append(muteButton);

7.4. PiP 종료

웹사이트는 사용자가 창의 닫기 버튼을 직접 클릭하지 않아도 DocumentPictureInPicture Window 를 닫고 싶을 수 있습니다. 이 경우, Window 객체에서 close() 메서드를 사용하면 됩니다:

// PiP 창을 닫고 onLeavePiP() 리스너가 동작함.
// 리스너 참조.
pipWindow.close();

7.5. PiP 창이 닫힐 때 요소 꺼내기

PiP 창이 어떤 이유로 닫히든(사이트가 닫든 사용자가 창을 닫든) 사이트는 PiP 창에서 요소를 다시 꺼내야 할 때가 많습니다. 이러한 처리는 pagehide 이벤트 핸들러에서 할 수 있습니다. onLeavePiP() 핸들러는 위의 비디오 플레이어 예시에 있으며 아래에 복사되었습니다:

// PiP 창이 닫힐 때 호출됨.
function onLeavePiP() {
  if (this !== pipWindow) {
    return;
  }

  // 컨테이너에서 PiP 스타일 해제.
  const playerContainer = document.querySelector('#player-container');
  playerContainer.classList.remove('pip-mode');

  // 플레이어를 본 창에 다시 추가.
  const player = pipWindow.document.querySelector('#player');
  playerContainer.append(player);

  pipWindow = null;
}

7.6. PiP 창 프로그래밍적 크기 조정

문서 picture-in-picture 창에서는 resizeTo()resizeBy() API를 지원하지만 PiP 창에서의 사용자 제스처가 필요합니다:

const expandButton = pipWindow.document.createElement('button');
expandButton.textContent = 'PiP 창 확대';
expandButton.addEventListener('click', () => {
  // PiP 창의 너비 20px, 높이 30px 확대.
  pipWindow.resizeBy(20, 30);
});
pipWindow.document.body.append(expandButton);

7.7. Opener 탭으로 돌아가기

focus() API로 picture-in-picture 창에서 오프너 탭을 사용자 제스처로 포커스할 수 있습니다:

const returnToTabButton = pipWindow.document.createElement('button');
returnToTabButton.textContent = '오프너 탭으로 이동';
returnToTabButton.addEventListener('click', () => {
  window.focus();
});
pipWindow.document.body.append(returnToTabButton);

7.8. CSS picture-in-picture display mode 사용

아래 예시는 PiP 창에서 body 엘리먼트의 margin을 제거하고, 타이틀의 폰트 크기를 더 작게 하여 PiP 창의 컨텐츠에 더 잘 맞추는 방법을 보여줍니다:

@media all and (display-mode: picture-in-picture) {
  body {
    margin: 0;
  }
  h1 {
    font-size: 0.8em;
  }
}

7.9. Opener로 돌아가기 버튼 숨기기

대부분의 사용자 에이전트는 비디오 및 문서 picture-in-picture 창에서 오프너로 돌아가고 창을 닫는 버튼을 표시하는데, 모든 사이트의 문서 PiP UX에 이 버튼이 항상 적합하지는 않습니다. disallowReturnToOpener 옵션을 사용하여 버튼을 숨길 수 있습니다.

await documentPictureInPicture.requestWindow({
  disallowReturnToOpener: true
});

7.10. 초기 창 배치 우선

문서 picture-in-picture 창이 열려 있을 때 사용자가 직접 크기 조정/위치 옮기기를 할 수도 있습니다. 창을 닫았다가 나중에 다시 열면 사용자 에이전트는 이전 위치와 크기를 새로운 창의 배치 힌트로 사용할 수 있습니다.

사이트는 preferInitialWindowPlacement 값을 true로 지정하여 이전 위치/크기를 사용하지 않아야 한다는 힌트를 사용자 에이전트에게 줄 수 있습니다. 예를 들어 이전과 무관한 PiP 창을 요청할 때 이렇게 할 수 있으며, 이 경우 새 창은 기본 위치, 기본 크기나 사이트가 지정한 크기 힌트를 사용하게 됩니다.

await documentPictureInPicture.requestWindow({
  preferInitialWindowPlacement: true
});

8. 감사의 글

Frank Liberato, Mark Foltz, Klaus Weidner, François Beaufort, Charlie Reis, Joe DeBlasio, Domenic Denicola, Yiren Wang께 본 명세와 그 논의를 통해 주신 의견과 기여에 감사드립니다.

적합성

문서 관례

적합성 요구사항은 설명적 단언과 RFC 2119 용어를 조합하여 표현됩니다. 규범적 부분에서 “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, “OPTIONAL” 등의 용어는 RFC 2119에서 설명된 대로 해석해야 합니다. 하지만 가독성을 위해 본 명세에서는 해당 용어가 모두 대문자로 표시되지 않습니다.

이 명세의 모든 텍스트는 특별히 비규범적, 예시, 또는 주로 명시된 부분을 제외하고는 규범적입니다. [RFC2119]

본 명세의 예시는 “for example” 문장으로 시작하거나 class="example"와 같이 규범적 텍스트와 구분되어 나타납니다:

이것은 참고용 예시입니다.

참고용 노트는 “Note”로 시작하며 class="note" 등으로 규범적 텍스트와 구분되어 나타납니다:

참고: 이것은 참고용 노트입니다.

색인

이 명세에서 정의된 용어

참조로 정의된 용어

참고문헌

규범적 참고문헌

[CSSOM-VIEW-1]
Simon Fraser; Emilio Cobos Álvarez. CSSOM 뷰 모듈. URL: https://drafts.csswg.org/cssom-view/
[DOM]
Anne van Kesteren. DOM 표준. Living Standard. URL: https://dom.spec.whatwg.org/
[FULLSCREEN]
Philip Jägenstedt. Fullscreen API 표준. Living Standard. URL: https://fullscreen.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML 표준. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra 표준. Living Standard. URL: https://infra.spec.whatwg.org/
[MEDIAQUERIES-5]
Dean Jackson; et al. 미디어 쿼리 레벨 5. URL: https://drafts.csswg.org/mediaqueries-5/
[RFC2119]
S. Bradner. RFC에서 요구 수준을 나타내기 위한 주요 용어(Key words for use in RFCs to Indicate Requirement Levels). 1997년 3월. Best Current Practice. URL: https://datatracker.ietf.org/doc/html/rfc2119
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL 표준. Living Standard. URL: https://webidl.spec.whatwg.org/

비규범적 참고문헌

[SECURE-CONTEXTS]
Mike West. Secure Contexts. URL: https://w3c.github.io/webappsec-secure-contexts/

IDL 색인

[Exposed=Window]
partial interface Window {
  [SameObject, SecureContext] readonly attribute DocumentPictureInPicture
    documentPictureInPicture;
};

[Exposed=Window, SecureContext]
interface DocumentPictureInPicture : EventTarget {
  [NewObject] Promise<Window> requestWindow(
    optional DocumentPictureInPictureOptions options = {});
  readonly attribute Window window;
  attribute EventHandler onenter;
};

dictionary DocumentPictureInPictureOptions {
  [EnforceRange] unsigned long long width = 0;
  [EnforceRange] unsigned long long height = 0;
  boolean disallowReturnToOpener = false;
  boolean preferInitialWindowPlacement = false;
};

[Exposed=Window, SecureContext]
interface DocumentPictureInPictureEvent : Event {
  constructor(DOMString type, DocumentPictureInPictureEventInit eventInitDict);
  [SameObject] readonly attribute Window window;
};

dictionary DocumentPictureInPictureEventInit : EventInit {
  required Window window;
};

이슈 색인

이 내용을 definitely close 항목에 컨센서스가 충분해지면 병합하세요.
이 내용을 destroy 항목에 컨센서스가 충분해지면 병합하세요.
이 내용을 navigate 항목에 컨센서스가 충분해지면 병합하세요.
이 내용을 resizeTo() 항목에 컨센서스가 충분해지면 병합하세요.
이 내용을 resizeBy() 항목에 컨센서스가 충분해지면 병합하세요.
이 내용을 moveTo()moveBy() 단계에 컨센서스가 충분해지면 병합하세요.
이 내용을 focus() 항목에 컨센서스가 충분해지면 병합하세요.
이 내용을 activation notification 단계에 컨센서스가 충분해지면 병합하세요.
이 내용을 consume user activation 단계에 컨센서스가 충분해지면 병합하세요.
이 내용을 fullscreen is supported 항목에 컨센서스가 충분해지면 병합하세요.