Shaka Player 파헤치기 - 에러 Severity와 자체 회복 로직

영상 플레이어를 운영하다 보면 콘솔이나 에러 로그에는 Shaka 에러가 찍혔는데 영상은 멀쩡히 재생되고 있는 상황을 자주 만납니다. 반대로 에러 하나에 재생이 완전히 멈추기도 합니다. 이 차이를 이해하려면 Shaka Player가 에러를 어떻게 분류하고, 어떤 에러를 스스로 회복하는지 알아야 합니다.

이 글에서는 Shaka Player 5.3.4 소스코드를 따라가며 다음을 확인합니다.

  • 에러 객체의 구조와 주요 에러 종류
  • CRITICAL, RECOVERABLE Severity가 정해지고 바뀌는 과정
  • 플레이어가 자체 회복하는 에러와 그 경로
  • 개발자가 직접 다루어야 하는 에러와 처리 패턴

결론부터 말하면, 재생 파이프라인에서 난 에러는 대부분 플레이어가 먼저 회복을 시도합니다. 다만 RECOVERABLE이라고 해서 모두 자동으로 회복되는 것은 아닙니다. API를 잘못 호출해서 난 에러는 RECOVERABLE이어도 애플리케이션이 처리해야 합니다.

Shaka Player의 에러 구조

Shaka Player가 던지는 에러는 모두 shaka.util.Error 클래스의 인스턴스입니다.

1// lib/util/error.js — shaka.util.Error (발췌)
2shaka.util.Error = class {
3  constructor(severity, category, code, ...varArgs) {
4    this.severity = severity;
5    this.category = category;
6    this.code = code;
7    this.data = varArgs;
8    this.handled = false;
9    // ...
10  }
11};
필드의미
severity심각도. RECOVERABLE(1) 또는 CRITICAL(2)
category에러가 발생한 영역. NETWORK(1)부터 ADS(10)까지
code구체적인 에러 번호
data에러 코드별 부가 정보를 담은 배열
handled에러가 이미 처리되었는지를 나타내는 값
message컴파일 빌드에서는 Shaka Error 1001처럼 번호만, 디버그 빌드에서는 카테고리와 코드 이름까지 표시

data의 구조는 코드마다 다릅니다. 예를 들어 BAD_HTTP_STATUS(1001)는 다음 값을 담습니다.

인덱스값
data[0]요청 URI
data[1]HTTP 상태 코드
data[2]응답 본문 텍스트
data[3]응답 헤더
data[4]요청 타입(NetworkingEngine.RequestType)
data[5]리다이렉트를 거친 최종 URI
INFO

shaka.util.Error는 네이티브 Error를 상속하지 않습니다. 소스 주석에 따르면 instanceof Error가 false가 되도록 의도한 것으로, 애플리케이션이 처리되지 않은 네이티브 에러와 Shaka 에러를 구분할 수 있게 하기 위함입니다.

Shaka Player의 주요 에러 종류

에러는 10개의 카테고리로 나뉘고, 코드 번호의 앞자리는 대체로 카테고리 번호와 일치합니다.

카테고리영역대표 코드
NETWORK (1)네트워크BAD_HTTP_STATUS 1001, HTTP_ERROR 1002, TIMEOUT 1003, SEGMENT_MISSING 1011
TEXT (2)자막 파싱INVALID_TEXT_HEADER 2000, TEXT_COULD_NOT_GUESS_MIME_TYPE 2011
MEDIA (3)오디오/비디오 처리BUFFER_READ_OUT_OF_BOUNDS 3000, MEDIA_SOURCE_OPERATION_FAILED 3014, MEDIA_SOURCE_OPERATION_THREW 3015, VIDEO_ERROR 3016, QUOTA_EXCEEDED_ERROR 3017, TRANSMUXING_FAILED 3018
MANIFEST (4)매니페스트 파싱UNABLE_TO_GUESS_MANIFEST_TYPE 4000, DASH_INVALID_XML 4001, RESTRICTIONS_CANNOT_BE_MET 4012, HLS_PLAYLIST_HEADER_MISSING 4015, CONTENT_UNSUPPORTED_BY_BROWSER 4032, NO_VARIANTS 4036
STREAMING (5)스트리밍STREAMING_ENGINE_STARTUP_INVALID_STATE 5006
DRM (6)DRMNO_RECOGNIZED_KEY_SYSTEMS 6000, REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE 6001, LICENSE_REQUEST_FAILED 6007, LICENSE_RESPONSE_REJECTED 6008, EXPIRED 6014
PLAYER (7)플레이어 일반LOAD_INTERRUPTED 7000, OPERATION_ABORTED 7001, NO_VIDEO_ELEMENT 7002, CONTENT_NOT_LOADED 7004
CAST (8)CastCAST_API_UNAVAILABLE 8000
STORAGE (9)오프라인 저장소STORAGE_NOT_SUPPORTED 9000
ADS (10)광고CS_IMA_SDK_MISSING 10000

운영 중에 자주 살펴보게 되는 코드의 의미는 다음과 같습니다.

  • BAD_HTTP_STATUS: 서버가 실패 상태 코드를 응답했습니다. 404, 403처럼 상태 코드의 종류를 가리지 않습니다.
  • HTTP_ERROR: 서버 응답이 아닌 이유로 요청이 실패했습니다. 연결 실패나 CORS 차단이 여기에 해당할 수 있습니다.
  • MEDIA_SOURCE_OPERATION_FAILED, MEDIA_SOURCE_OPERATION_THREW: MSE 동작이 실패했거나 예외를 던졌습니다.
  • VIDEO_ERROR: <video> 요소가 에러를 보고했습니다. 브라우저의 MediaError 코드가 data에 담깁니다.
  • RESTRICTIONS_CANNOT_BE_MET: 재생 가능한 Variant가 제한 조건에 모두 걸렸습니다.
  • LOAD_INTERRUPTED: 진행 중인 load()가 unload()나 다른 load()에 의해 중단되었습니다.

CRITICAL, RECOVERABLE은 어떻게 설정되고 바뀌는가

소스코드는 두 Severity를 다음과 같이 정의합니다.

1// lib/util/error.js — Severity (주석 번역)
2shaka.util.Error.Severity = {
3  // 에러가 발생했지만 Player가 회복을 시도하고 있습니다.
4  'RECOVERABLE': 1,
5
6  // 라이브러리가 회복할 수 없는 치명적인 에러입니다.
7  // 보통 Player의 로딩이나 갱신이 멈추며, 라이브러리를 초기화하려면
8  // 새 매니페스트를 로드해야 합니다.
9  'CRITICAL': 2,
10};

Severity는 에러를 생성할 때 생성자의 첫 번째 인자로 정해집니다. 소스 전체에서 Severity.CRITICAL은 247번, Severity.RECOVERABLE은 38번 등장합니다. 그런데 severity는 고정된 값이 아닙니다. 에러가 플레이어 내부를 지나는 동안 맥락에 따라 다시 쓰입니다.

세그먼트 요청 하나가 실패할 때를 따라가 보면 값이 이렇게 바뀝니다.

순서위치상황Severity
1HTTP 플러그인요청 실패RECOVERABLE
2NetworkingEngine재시도 소진CRITICAL
3StreamingEngine세그먼트 가져오기/추가 실패 처리CRITICAL
4StreamingEngine대체 스트림으로 전환 성공RECOVERABLE

1. HTTP 플러그인 — 일단 RECOVERABLE

HTTP 요청이 실패하면 플러그인은 RECOVERABLE 에러를 만듭니다. 상태 코드를 구분하지 않기 때문에 404나 403도 처음에는 RECOVERABLE이고 재시도 대상입니다.

1// lib/net/http_plugin_utils.js — makeResponse (발췌)
2throw new shaka.util.Error(
3    shaka.util.Error.Severity.RECOVERABLE,
4    shaka.util.Error.Category.NETWORK,
5    shaka.util.Error.Code.BAD_HTTP_STATUS,
6    uri, status, responseText, headers, requestType, responseURL || uri);

2. NetworkingEngine — 재시도를 소진하면 CRITICAL

재시도까지 모두 실패해 NetworkingEngine 밖으로 나가는 에러는 예외 없이 CRITICAL로 다시 분류됩니다.

1// lib/net/networking_engine.js — request (발췌)
2// Any error thrown from elsewhere should be recategorized as CRITICAL
3// here.  This is because by the time it gets here, we've exhausted
4// retries.
5if (e) {
6  e.severity = shaka.util.Error.Severity.CRITICAL;
7}
8throw e;
WARNING

error.js의 RECOVERABLE 주석에는 "세그먼트 재시도는 절대 CRITICAL이 되지 않는다(계속 재시도한다)"라고 적혀 있습니다. 하지만 기본 설정은 maxAttempts: 2이고, 위 코드에 따라 재시도가 끝나면 CRITICAL이 됩니다. 주석의 설명은 maxAttempts를 무한대로 설정했을 때에 해당합니다.

3. StreamingEngine — 실패한 에러를 CRITICAL로 확정

StreamingEngine은 세그먼트를 가져와 추가하는 과정의 실패를 받아, 별도로 처리하는 경우(요청 중단, 무시 설정된 자막 실패, 버퍼 용량 초과)를 빼고는 CRITICAL로 표시합니다. RECOVERABLE로 만들어진 SEGMENT_MISSING도 여기서 CRITICAL이 됩니다.

1// lib/media/streaming_engine.js — fetchAndAppend_ catch (발췌)
2mediaState.hasError = true;
3
4error.severity = shaka.util.Error.Severity.CRITICAL;
5await this.handleStreamingError_(mediaState, error);

4. 회복에 성공하면 다시 RECOVERABLE

반대로 플레이어가 회복할 방법을 찾으면 Severity를 낮춥니다. 낮추는 지점은 세 곳입니다.

위치조건
StreamingEngine.handleStreamingError_문제 스트림을 비활성화하고 대체 스트림으로 전환할 수 있을 때
Player.defaultStreamingFailureCallback_라이브 스트림에서 BAD_HTTP_STATUS, HTTP_ERROR, TIMEOUT이 발생했을 때
DASH, HLS 파서의 매니페스트 갱신갱신 요청이 실패해 다시 시도할 때

DRM의 일부 에러는 설정에 따라 Severity가 결정되기도 합니다. drm.persistentSessionOnlinePlayback이 켜져 있으면 저장된 세션을 불러오지 못해도 RECOVERABLE, 꺼져 있으면(기본값) CRITICAL입니다.

Shaka Player가 자체 회복하는 에러

재생 중 발생하는 에러의 회복 경로를 하나씩 살펴보겠습니다.

네트워크 요청 재시도

모든 요청은 NetworkingEngine에서 재시도됩니다. 기본값은 lib/net/backoff.js에 정의되어 있고, streaming, manifest, drm의 retryParameters로 각각 변경할 수 있습니다.

설정기본값의미
maxAttempts2첫 시도를 포함한 총 시도 횟수
baseDelay1000첫 재시도 전 대기 시간(ms)
backoffFactor2재시도마다 대기 시간에 곱하는 값
fuzzFactor0.5대기 시간을 ±50% 범위에서 무작위로 조정
timeout30000요청 전체 제한 시간(ms)
stallTimeout5000데이터 수신이 멈췄을 때의 제한 시간(ms)
connectionTimeout10000연결 제한 시간(ms)

재시도할 때는 retry 이벤트가 발생합니다. 재시도 중에는 Player의 error 이벤트가 발생하지 않습니다. 공식 튜토리얼도 "실패한 네트워크 요청의 재시도는 RECOVERABLE 에러로 보고되지 않는다"고 명시합니다.

대체 스트림으로 전환

재시도가 끝난 뒤에도 실패하면 StreamingEngine은 문제 스트림을 비활성화하고 다른 화질로 전환합니다. 이때 Severity가 RECOVERABLE로 낮아집니다.

1// lib/media/streaming_engine.js — handleStreamingError_ (발췌)
2// Try to recover from media source and network errors, but not timeouts.
3if (error.code == shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW ||
4    error.code == shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED ||
5    (error.category === shaka.util.Error.Category.NETWORK &&
6    error.code != shaka.util.Error.Code.TIMEOUT)) {
7  // ...
8  const disabledStream = this.playerInterface_.disableStream(
9      mediaState.stream, maxDisabledTime);
10  error.handled = disabledStream || mediaSourceReset;
11
12  // Decrease the error severity to recoverable
13  if (disabledStream) {
14    error.severity = shaka.util.Error.Severity.RECOVERABLE;
15  }
16}

비활성화된 스트림은 streaming.maxDisabledTime(기본 30초)이 지나면 다시 후보로 돌아옵니다. MSE 동작 실패가 원인이면 비활성화 시간은 무한대입니다.

전환은 항상 가능한 것이 아닙니다. Player.disableStream은 대표적으로 다음 경우에 false를 돌려주고, 이때는 스트림 전환이 일어나지 않습니다.

  • abr.enabled가 false인 경우
  • navigator.onLine이 false인 경우
  • 대체 스트림이 없는 경우. 단일 화질 콘텐츠가 대표적이고, 오디오는 같은 언어의 다른 스트림이 있어야 합니다.

즉 ABR을 끄고 화질을 고정한 플레이어에서는 이 회복 경로가 동작하지 않습니다. TIMEOUT 에러도 코드에서 볼 수 있듯이 이 경로에서 제외됩니다.

세그먼트 누락 건너뛰기

SEGMENT_MISSING은 세그먼트가 없다고 표시된 경우에 발생합니다. 위의 스트림 전환으로 처리되지 않으면 StreamingEngine은 누락되지 않은 다음 세그먼트까지 건너뛰어 스트리밍을 이어 갑니다. 건너뛸 세그먼트를 찾은 경우에는 error 이벤트를 발생시키지 않습니다.

버퍼 용량 초과 대응

QUOTA_EXCEEDED_ERROR는 SourceBuffer에 세그먼트를 넣을 공간이 없을 때 발생합니다. 플레이어는 이미 재생한 구간의 버퍼를 비우고, 버퍼링 목표를 단계적으로 줄이면서 다시 시도합니다.

1// lib/media/streaming_engine.js — handleQuotaExceeded_ (발췌)
2// Reduction schedule: 80%, 60%, 40%, 20%, 16%, 12%, 8%, 4%, fail.
3const percentBefore = Math.round(100 * this.bufferingScale_);
4if (percentBefore > 20) {
5  this.bufferingScale_ -= 0.2;
6} else if (percentBefore > 4) {
7  this.bufferingScale_ -= 0.04;
8}

이 과정에서는 애플리케이션에 에러가 전달되지 않고, 4%까지 줄여도 해결되지 않을 때 스트림 전환을 시도합니다.

MediaSource 리셋

MSE 계열 에러는 MediaSource를 다시 만들어 회복합니다. 경로는 두 가지입니다.

StreamingEngine은 세그먼트 추가가 실패하면, 이미 망가졌을 수 있는 SourceBuffer에 다시 데이터를 넣지 않도록 먼저 MediaSource를 강제로 리셋하고 다른 스트림으로 전환합니다.

Player의 onError_도 에러를 앱에 알리기 전에 리셋을 시도합니다. 대상은 VIDEO_ERROR, MEDIA_SOURCE_OPERATION_FAILED, MEDIA_SOURCE_OPERATION_THREW, STREAMING_NOT_ALLOWED, TRANSMUXING_FAILED입니다. 리셋에 성공하면 error 이벤트 대신 mediasourcerecovered 이벤트가 발생합니다.

1// lib/player.js — onError_ (발췌)
2const ret = await this.streamingEngine_.resetMediaSource();
3if (ret) {
4  const event = shaka.Player.makeEvent_(
5      shaka.util.FakeEvent.EventName.MediaSourceRecovered);
6  this.dispatchEvent(event);
7  return;
8}

이 경로의 리셋은 streaming.allowMediaSourceRecoveries(기본 true)와 streaming.minTimeBetweenRecoveries(기본 5초)의 영향을 받습니다. 5초 안에 다시 실패하면 리셋하지 않고 error 이벤트로 넘어갑니다.

매니페스트 갱신 실패

라이브 스트림은 매니페스트를 주기적으로 갱신합니다. 갱신 요청이 재시도까지 모두 실패해도 파서가 Severity를 RECOVERABLE로 낮추고 다시 시도합니다.

1// lib/hls/hls_parser.js (발췌)
2// We will retry updating, so override the severity of the error.
3error.severity = shaka.util.Error.Severity.RECOVERABLE;
4this.playerInterface_.onError(error);
5
6// Try again very soon.
7this.updatePlaylistTimer_.tickAfter(/* seconds= */ 0.1);

DASH 파서도 같은 방식으로 동작합니다. manifest.raiseFatalErrorOnManifestUpdateRequestFailure를 켜면 다시 시도하지 않고 에러를 그대로 전달합니다.

라이브 스트림 자동 재시도

대체 스트림이 없을 때 라이브와 VOD의 동작이 갈립니다. Player의 기본 failureCallback은 라이브에서만 스트리밍을 다시 시도합니다.

1// lib/player.js — defaultStreamingFailureCallback_ (발췌)
2// For live streams, we retry streaming automatically for certain errors.
3// For VOD streams, all streaming failures are fatal.
4if (!this.isDynamic()) {
5  return;
6}
7
8let retryDelaySeconds = null;
9if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS ||
10    error.code == shaka.util.Error.Code.HTTP_ERROR) {
11  retryDelaySeconds = 1;
12  if (this.config_.streaming.lowLatencyMode) {
13    retryDelaySeconds = 0.1;
14  }
15} else if (error.code == shaka.util.Error.Code.TIMEOUT) {
16  retryDelaySeconds = 0.1;
17}
18
19if (retryDelaySeconds != null) {
20  error.severity = shaka.util.Error.Severity.RECOVERABLE;
21  this.retryStreaming(retryDelaySeconds);
22}

또한 Player는 브라우저의 online 이벤트를 듣고 있어서, 네트워크가 돌아오면 비활성화된 스트림을 복원하고 retryStreaming()을 호출합니다. 이 동작은 VOD에도 적용됩니다.

회복 경로와 애플리케이션에 보이는 신호

회복 경로애플리케이션에 보이는 것
네트워크 재시도NetworkingEngine의 retry 이벤트
대체 스트림 전환error 이벤트(RECOVERABLE). SEGMENT_MISSING은 발생하지 않음
세그먼트 건너뛰기없음
버퍼 용량 초과 대응없음
Player의 MediaSource 리셋mediasourcerecovered 이벤트
매니페스트 갱신 재시도error 이벤트(RECOVERABLE)
라이브 자동 재시도error 이벤트(CRITICAL로 보임)

마지막 줄은 주의가 필요합니다. handleStreamingError_는 error 이벤트를 먼저 발생시키고 그다음에 failureCallback을 호출합니다. 라이브 재시도에서 Severity를 낮추는 일은 failureCallback 안에서 일어나므로, error 리스너가 받는 시점의 Severity는 아직 CRITICAL입니다. 플레이어는 곧이어 재시도하는데도 말입니다.

Shaka Player가 자체 회복하지 못하고 개발자가 직접 다루어야 하는 에러

RECOVERABLE이지만 회복 로직이 없는 에러

RECOVERABLE이 항상 "플레이어가 알아서 고친다"는 뜻은 아닙니다. 소스에서 RECOVERABLE로 직접 생성하는 32곳 중 18곳은 재시도나 전환 없이 API를 호출한 쪽에 에러를 그대로 돌려줍니다.

API 영역대표 에러발생 상황
addTextTrackAsync, addThumbnailsTrack, addChaptersTrackCONTENT_NOT_LOADEDload()가 끝나기 전에 호출
addTextTrackAsyncCANNOT_ADD_EXTERNAL_TEXT_TO_LIVE_STREAM라이브 스트림에 외부 자막 추가
트랙 추가 APITEXT_COULD_NOT_GUESS_MIME_TYPE파일의 MIME 타입을 알 수 없음
CastCAST_API_UNAVAILABLE, NO_CAST_RECEIVERS, ALREADY_CASTINGCast를 사용할 수 없는 상태
광고CURRENT_DAI_REQUEST_NOT_FINISHED, MEDIATAILOR_REQUEST_FAILED광고 스트림 요청 실패 또는 중복

이 에러들은 호출 하나가 실패했을 뿐 재생 자체에는 영향이 없다는 쪽에 가깝습니다. 호출 코드의 try/catch나 .catch()에서 직접 처리해야 합니다.

로드 단계의 CRITICAL

load() 중에 난 에러는 load()의 Promise를 reject합니다. 이때 플레이어는 내부적으로 unload()까지 수행하므로, 다시 재생하려면 load()를 새로 호출해야 합니다.

  • 매니페스트: UNABLE_TO_GUESS_MANIFEST_TYPE, DASH_INVALID_XML, HLS_PLAYLIST_HEADER_MISSING, NO_VARIANTS
  • 재생 환경: CONTENT_UNSUPPORTED_BY_BROWSER, RESTRICTIONS_CANNOT_BE_MET
  • DRM 설정: NO_RECOGNIZED_KEY_SYSTEMS, REQUESTED_KEY_SYSTEM_CONFIG_UNAVAILABLE
  • API 사용 순서: NO_VIDEO_ELEMENT(attach() 전에 load() 호출)
  • 매니페스트 요청 실패: 재시도를 소진한 첫 매니페스트 요청

LOAD_INTERRUPTED도 CRITICAL이지만, 다른 load()나 unload()가 끼어들었다는 의미이므로 보통은 에러 화면을 띄우지 않고 무시합니다.

재생 중의 CRITICAL

  • VOD의 네트워크 실패: 재시도를 소진했고 대체 스트림도 없으면 기본 failureCallback은 아무것도 하지 않고, 해당 스트림의 스트리밍이 멈춥니다. player.retryStreaming()으로 재개할 수 있습니다.
  • 라이선스 문제: LICENSE_REQUEST_FAILED, LICENSE_RESPONSE_REJECTED, EXPIRED
  • 미디어 처리 실패: MediaSource 리셋으로도 넘기지 못한 VIDEO_ERROR, MSE 계열 에러
  • 자막 실패: streaming.ignoreTextStreamFailures의 기본값이 false이므로, 자막 세그먼트 실패도 다른 스트림 실패와 같은 경로를 탑니다. true로 설정하면 자막 없이 재생을 이어 갑니다.
  • 버퍼 용량 초과의 최종 실패: 버퍼 축소와 스트림 전환이 모두 실패하면 fatalError_가 설정됩니다. 이 상태에서는 retryStreaming()도 false를 반환하므로 콘텐츠를 다시 로드해야 합니다.

애플리케이션 에러 처리 패턴

error 이벤트와 load()를 모두 처리하기

재생 중 에러는 error 이벤트로, 로드 중 에러는 load()의 Promise로 전달됩니다. 두 경로를 같은 함수로 받으면 분기가 단순해집니다.

1const { BAD_HTTP_STATUS, HTTP_ERROR, TIMEOUT, LOAD_INTERRUPTED } =
2  shaka.util.Error.Code;
3const liveRetryCodes = [BAD_HTTP_STATUS, HTTP_ERROR, TIMEOUT];
4
5const handleError = (error) => {
6  if (error instanceof Error) {
7    // Shaka 에러가 아닌 네이티브 에러
8    reportUnexpected(error);
9    return;
10  }
11
12  // 다른 load()나 unload()가 끼어든 경우
13  if (error.code === LOAD_INTERRUPTED) {
14    return;
15  }
16
17  if (error.severity !== shaka.util.Error.Severity.CRITICAL) {
18    reportRecoverable(error);
19    return;
20  }
21
22  // 라이브의 네트워크 에러는 기본 failureCallback이 곧이어 재시도합니다.
23  if (player.isDynamic() && liveRetryCodes.includes(error.code)) {
24    reportRecoverable(error);
25    return;
26  }
27
28  showErrorScreen(error);
29};
30
31player.addEventListener('error', (event) => handleError(event.detail));
32
33try {
34  await player.load(url);
35} catch (error) {
36  handleError(error);
37}

앞서 본 것처럼 라이브 자동 재시도의 경우 error 이벤트의 CRITICAL이 곧 재생 종료를 의미하지 않습니다. 그래서 위 코드는 라이브의 네트워크 에러를 에러 화면 대상에서 제외합니다.

failureCallback으로 스트리밍 실패 처리하기

streaming.failureCallback은 플레이어가 처리하지 못한 스트리밍 에러에 대해 호출되며, 스트리밍 실패 시의 정책을 바꾸고 싶을 때 사용합니다. 이 설정을 바꾸면 기본 콜백을 대체하므로, 앞에서 본 라이브 자동 재시도도 함께 사라집니다. 라이브 재시도가 필요하면 직접 호출해야 합니다.

1player.configure('streaming.failureCallback', (error) => {
2  if (player.isDynamic() && liveRetryCodes.includes(error.code)) {
3    player.retryStreaming(1);
4    return;
5  }
6
7  // VOD에서는 네트워크 복구 후 재시도하는 등 서비스 정책을 적용합니다.
8  applyStreamingFailurePolicy(error);
9});

failureCallback은 세그먼트 스트리밍 과정의 에러만 받습니다. DRM 에러나 버퍼 용량 초과의 최종 실패처럼 다른 경로로 전달되는 CRITICAL은 호출되지 않으므로, error 리스너는 항상 함께 등록해야 합니다.

WARNING

error 이벤트는 취소할 수 없는 이벤트입니다. event.preventDefault()를 호출해도 아무 효과가 없으므로, 이 방법으로 failureCallback호출을 막을 수는 없습니다.

retry 이벤트로 재시도 중단하기

재시도해도 소용없는 요청은 retry 이벤트에서 끊을 수 있습니다. 이 이벤트는 취소가 가능해서 preventDefault()를 호출하면 재시도가 바로 중단됩니다. 다음은 VOD 매니페스트가 404일 때 재시도하지 않는 예시입니다.

1const networkingEngine = player.getNetworkingEngine();
2
3networkingEngine.addEventListener('retry', (event) => {
4  const { code, data } = event.error;
5  const { BAD_HTTP_STATUS } = shaka.util.Error.Code;
6  const { MANIFEST } = shaka.net.NetworkingEngine.RequestType;
7
8  if (code === BAD_HTTP_STATUS && data[1] === 404 && data[4] === MANIFEST) {
9    event.preventDefault();
10  }
11});

반대로 일시적인 네트워크 불안정에 더 강하게 대응하고 싶다면 재시도 횟수를 늘릴 수 있습니다.

1player.configure('streaming.retryParameters.maxAttempts', 4);

회복된 에러도 지표로 남기기

자체 회복된 에러는 사용자에게 드러나지 않지만, 쌓이면 품질 저하의 신호가 됩니다. error 이벤트의 RECOVERABLE과 mediasourcerecovered 이벤트를 수집하고, 누적 값은 player.getStats().nonFatalErrorCount로 확인할 수 있습니다.

마치며, 정리

  • Shaka Player의 에러는 severity, category, code, data, handled 로 구성되며, 네이티브 Error를 상속하지 않습니다.
  • severity는 생성 시점에 정해지지만 고정된 값이 아닙니다. 네트워크 재시도를 소진하면 CRITICAL이 되고, 대체 스트림 전환에 성공하면 다시 RECOVERABLE이 됩니다.
  • 재생 파이프라인의 에러는 플레이어가 재시도, 스트림 전환, 세그먼트 건너뛰기, 버퍼 축소, MediaSource 리셋, 매니페스트 재요청, 라이브 재시도로 대부분 먼저 회복을 시도합니다.
  • 자체 회복은 조건이 맞아야 동작합니다. 특히 ABR을 끄거나 대체 스트림이 없는 VOD에서는 스트리밍이 멈출 수 있습니다.
  • API 호출 에러는 RECOVERABLE이어도 호출한 쪽에서 처리해야 하고, 로드 단계의 CRITICAL은 load()를 다시 호출해야 합니다.
  • 라이브 자동 재시도 대상 에러는 error 이벤트에 CRITICAL로 보이므로, 에러 화면을 띄울 때는 Severity와 함께 스트림 종류와 에러 코드도 확인해야 합니다.