Unity에서 라이브 스트리밍 - 실시간 게임 브로드캐스팅 (Part 2) (Live Streaming from Unity - Broadcasting a Game in Real-Time (Part 2))
Tags: aws, amazonivs, gamedev, unity3d
Published: 2024-05-09 01:33:32

> _이 블로그 포스트는 [Todd Sharp](https://dev.to/recursivecodes)(Amazon IVS Developer Evangelist)이 포스팅한 [Live Streaming from Unity with Amazon IVS - Part 1](https://dev.to/aws/live-streaming-from-unity-with-amazon-ivs-part-1-52nc)의 한국어 번역입니다.
> (This is the Korean translation of Amazon IVS Developer Evangelist, [Todd Sharp](https://dev.to/recursivecodes)'s [Live Streaming from Unity with Amazon IVS - Part 1](https://dev.to/aws/live-streaming-from-unity-with-amazon-ivs-part-1-52nc).)_

이 포스팅에서는 Unity에서 제작한 게임을 Amazon IVS(Amazon Interactive Video Service)에서 제공하는 실시간 라이브 스트림으로 직접 브로드캐스팅하는 방법을 살펴보겠습니다. 이 포스팅에서는 Amazon IVS Stage를 만들고, 해당 Stage로 브로드캐스팅하는 데 필요한 토큰을 생성하는 등 향후 포스팅에서 재사용될 몇 가지 주제를 다루기 때문에 이 시리즈의 다른 포스팅보다 내용이 조금 더 많을 것입니다.

## 시작하기

이 포스팅에서는 Unity Hub에서 학습용 템플릿으로 제공되는 'Karting Microgame'을 활용하겠습니다.

Unity Hub에서 'New Project'를 클릭하세요.

![New project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ivab63ghinefymhqgd9t.png)

왼쪽 사이드바에서 'Learning'을 클릭하고 'Karting Microgame'을 선택한 다음, 프로젝트 이름을 `ivs-rtx-broadcast-demo`로 지정하고 'Create project'를 클릭하세요.

![Create project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s7qr3gzcflhbiq1ik51d.png)

## WebRTC 지원 추가하기 

Amazon IVS Stage로 브로드캐스팅(하고 Stage로부터 재생)하려면 WebRTC가 사용됩니다. 다행히도 이를 위해 사용할 수 있는 훌륭한 [Unity WebRTC package](https://docs.unity3d.com/Packages/com.unity.webrtc@3.0/manual/index.html)가 있으며 Amazon IVS가 [이제 WHIP 프로토콜을 지원하므로](https://docs.aws.amazon.com/ivs/latest/RealTimeUserGuide/obs-whip-support.html), 이를 활용하여 게임에서 직접 브로드캐스팅할 수 있습니다.

Karting 데모를 위해 WebRTC 패키지를 설치하려면 Window -> Package Manager로 이동하세요.

![Unity Package Manager](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jqzwvwpjja5ek5xrgce9.png)

Package Manager 대화 상자에서 'Add package from git URL'를 선택합니다.

![Add package from git URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kz4hec3uonc72w19os5a.png)

`com.unity.webrtc@3.0.0-pre.7` 을 Git URL로 입력하고 'Add'를 클릭합니다.

![Add the Git URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jnb4lgjmny5xuomx1vv4.png)

> ⚠️ 이 데모는 테스트를 거쳤으며 위에 나열된 패키지 버전에서 작동하는 것으로 알려져 있습니다. 이 글을 읽는 시점에는 최신 버전이 아닐 수도 있습니다.

설치가 완료되면 Package Manager 대화 상자에 WebRTC 패키지 세부 정보가 표시됩니다.

![WebRTC package details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bk9z3p7xxeazjrp1etcq.png)

게임플레이를 퍼블리시하는 데 필요한 카메라와 스크립트를 추가하기 전에, Amazon IVS Stage를 생성하고 Stage로 퍼블리시하는 데 필요한 참여자 토큰을 생성하는 방법을 설정해야 합니다.

## Stage 생성

Amazon IVS Stage에서는 최대 12명의 브로드캐스터가 최대 10,000명의 시청자에게 실시간 라이브 스트리밍을 할 수 있습니다. 모든 데모에서 이 Stage를 재사용할 것이므로 AWS Management Console을 통해 수동으로 생성하겠습니다. 모든 플레이어에게 이 기능을 통합하기로 결정한다면, AWS SDK를 통해 프로그래밍 방식으로 Stage를 생성하고 백엔드 서비스에서 현재 플레이어의 ARN(Amazon Resource Name)을 검색해야할 것입니다. 우리는 단지 작동 방식을 배우는 중이므로 수동으로 생성하고 데모 게임에 ARN을 하드코딩해도 아무런 문제가 없습니다.

AWS Management Console에서, 'Amazon Interactive Video Service'를 검색하세요. Amazon IVS Console 랜딩 페이지에서 'Amazon IVS Stage'를 선택하고 'Get Started'를 클릭하세요.

![Get started](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5xz9yjlq1bpysos8ume0.png)

Stage 이름을 입력하고 'Create statge'를 클릭하세요.

![Create stage details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rm3r7jgkkvntyflgpmgi.png)

이제 Stage가 준비되었습니다! Stage 세부 정보 페이지에서 Stage ARN을 가져옵니다. 

![Stage details](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t0odqqmmlvpqfds1fka3.png)

이제 토큰을 생성해야 합니다.

# 참가자 토큰

Amazon IVS Stage의 모든 참가자 - 브로드캐스터와 시청자 모두 - 는 Stage에 연결하기 위해 참가자 토큰이 필요합니다. 이것은 JWT 토큰으로, 사용자를 인증하는데 사용되고 `userid`와 같은 정보와 참가자에게 부여된 모든 기능(예: `PUBLISH` 및 `SUBSCRIBE`)을 포함합니다. 이러한 토큰을 수동으로 생성하고 Unity 코드에 반복적으로 붙여넣으려면 시간이 많이 걸리므로 AWS SDK를 사용하여 이러한 토큰을 생성하는 독립형 서비스를 만드는 것이 좋습니다. 저는 이와 같은 데모를 많이 제작하기 때문에 토큰 생성을 처리하기 위해 AWS Lambda를 배포했습니다. 여러분이 아직 이렇게 할 준비가 되지 않았다면, 로컬에서 [선호하는 언어]용 AWS SDK를 사용하는 서비스를 만들고 `CreateParticipantToken`([문서](https://docs.aws.amazon.com/ivs/latest/RealTimeAPIReference/API_CreateParticipantToken.html))을 활용하세요. 저는 JavaScript를 선호하므로 제 함수는 JavaScript(v3) 용 AWS SDK를 사용하고 `CreateParticipantTokenCommand`([docs](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/ivs-realtime/command/CreateParticipantTokenCommand/))를 실행합니다. 

이를 수행하는 가장 기본적인 방법은 디렉터리를 생성하고 다음 종속성을 설치하는 것입니다.

```shell
npm i @aws-sdk/client-ivs-realtime@latest @aws-sdk/credential-providers
```

그런 다음 `index.js`라는 파일을 생성합니다. 이 파일에서는 `/token`이라는 하나의 경로에 응답하는 Node.js를 이용한 매우 기본적인 웹 서버를 실행할 것입니다. 이 경로는 토큰을 생성하여 JSON으로 반환합니다. 참고로, `[YOUR STAGE ARN]` 대신에 여러분의 Stage ARN을 입력해야 합니다.

```js
import * as http from 'http';
import * as url from 'url';
import { fromIni } from "@aws-sdk/credential-providers";
import { CreateParticipantTokenCommand, IVSRealTimeClient } from "@aws-sdk/client-ivs-realtime";

const ivsRealtimeClient = new IVSRealTimeClient({ credentials: fromIni({ profile: 'recursivecodes' }) });

export const createStageToken = async (stageArn, attributes, capabilities, userId, duration) => {
  const createStageTokenRequest = new CreateParticipantTokenCommand({
    attributes,
    capabilities,
    userId,
    stageArn,
    duration,
  });
  const createStageTokenResponse = await ivsRealtimeClient.send(createStageTokenRequest);
  return createStageTokenResponse;
};

async function handler(req, res) {
  const parsedUrl = url.parse(req.url, true);
  if (parsedUrl.pathname === '/token') {
    let body = '';
    req.on('data', chunk => {
      body += chunk.toString();
    });
    req.on('end', async () => {
      const params = JSON.parse(body);
      res.writeHead(200, { 'Content-type': 'application/json' });
      const tokenResponse = await createStageToken(
        params.stageArn,
        params.attributes,
        params.capabilities,
        params.userId,
        params.duration,
      );
      res.write(JSON.stringify(tokenResponse));
      res.end();
    });
  }
  else {
    res.writeHead(404, { 'Content-type': 'text/plain' });
    res.write('404 Not Found');
    res.end();
  };
}

const server = http.createServer(handler);
server.listen(3000);
```

`node index.js`로 실행하고 `http://localhost:3000/token`을 호출하여 토큰을 생성합니다. 토큰이 필요할 때마다 Unity에서 이 엔드포인트를 사용할 것입니다. 다음은 토큰이 어떻게 생성되는지 보여주는 예시입니다:

```bash
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"stageArn": "[YOUR STAGE ARN]", "userId": "123456", "capabilities": ["PUBLISH", "SUBSCRIBE"], "attributes": {"username": "todd"}}' \
  http://localhost:3000/token | jq
```

이 명령은 다음과 같은 결과를 보여줍니다:

```json
{
  "$metadata": {
    "httpStatusCode": 200,
    "requestId": "...",
    "cfId": "...",
    "attempts": 1,
    "totalRetryDelay": 0
  },
  "participantToken": {
    "attributes": {
      "username": "todd"
    },
    "capabilities": [
      "PUBLISH",
      "SUBSCRIBE"
    ],
    "duration": 1200,
    "expirationTime": "2024-01-19T15:11:32.000Z",
    "participantId": "bWO1wUhGopye",
    "token": "eyJhbGciOiJLTVMiLCJ0eXAiOiJKV1QifQ....[truncated]",
    "userId": "1705605092467"
  }
}
```

## 카메라 추가하기

이제 Stage와 토큰을 생성하는 방법이 생겼으니, Stage에 게임플레이를 브로드캐스팅하는 데 사용할 새 카메라를 추가할 수 있습니다.  Unity에서 'Main Scene'을 확장해서 `CinemachineVirtualCamera`를 찾습니다. 이 카메라는 Karting Microgame 프로젝트에서 플레이어의 카트가 코스를 주행할 때 트랙 주변을 따라가는 데 사용됩니다. `CinemachineVirtualCamera`를 마우스 오른쪽 버튼으로 클릭하고 `Camera`를 추가합니다. 저는 `WebRTCPublishCamera`라고 이름을 지었습니다.

![Add camera](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6a7obnedx0vqft7ue2o4.png)

다음으로, 새로 생성한 카메라를 선택하고 'Inspector' 탭의 하단으로 스크롤합니다. 'Audio Listener'를 선택 해제하지 않으면 하나의 Scene에 여러 오디오 리스너가 있다는 오류가 콘솔에 표시됩니다. 이를 선택 해제하고, 'Add Component'를 클릭합니다.

![Camera inspector](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/52qij16nhve5y71uumvs.png)

'Add Component' 메뉴에서 'New Script'를 선택하고 스크립트 이름을 `WebRTCPublish`로 지정합니다. 'Create and Add'를 클릭합니다.

![Add camera script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bp3xzu3aaaal0nd0tjro.png)

스크립트가 추가되면 스크립트를 두 번 클릭하여 VS Code (또는 여러분이 설정한 편집기)에서 엽니다.

![Edit script](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d25rh4iprb7a9vsgpvoj.png)

이제 카메라를 Stage로 브로드캐스트하는데 사용할 스크립트를 편집할 수 있습니다.

## Amazon IVS Stage로 카메라 브로드캐스트하기

이제 Stage 토큰을 가져오고 카메라를 Stage에 게시하기 위한 WebRTC 연결을 설정하는 데 필요한 로직을 포함하도록 `WebRTCPublish.cs` 스크립트를 수정할 수 있습니다.

토큰 생성 서비스의 요청과 응답을 처리하는 데 도움이 되는 몇 가지 클래스를 만들겠습니다. 별도의 파일을 사용할 수도 있지만, 지금은 모든 것을 한 곳에 보관하기 위해 동일한 파일 내에 정의합니다.

```cs
[System.Serializable]
public class ParticipantToken
{
  public string token;
  public string participantId;
  public System.DateTime expirationTime;
  public static ParticipantToken CreateFromJSON(string jsonString)
  {
    return JsonUtility.FromJson<ParticipantToken>(jsonString);
  }
}

[System.Serializable]
public class StageToken
{
  public ParticipantToken participantToken;
  public static StageToken CreateFromJSON(string jsonString)
  {
    return JsonUtility.FromJson<StageToken>(jsonString);
  }
}

[System.Serializable]
public class StageTokenRequestAttributes
{
  public string username;
  public StageTokenRequestAttributes(string username)
  {
    this.username = username;
  }
}

[System.Serializable]
public class StageTokenRequest
{
  public string stageArn;
  public string userId;
  public int duration;
  public StageTokenRequestAttributes attributes;
  public string[] capabilities;
  public StageTokenRequest(string stageArn, string userId, int duration, string[] capabilities, StageTokenRequestAttributes attributes)
  {
    this.stageArn = stageArn;
    this.userId = userId;
    this.duration = duration;
    this.capabilities = capabilities;
    this.attributes = attributes;
  }
}
```

다음으로 게임 오디오를 브로드캐스트하기 위해 `AudioListener`가 필요하다고 클래스에 Annotation을 추가해야 합니다. 또한 스크립트에서 사용할 몇 가지 변수를 선언할 것입니다.

```cs
[RequireComponent(typeof(AudioListener))]
public class WebRTCPublish : MonoBehaviour
{
  RTCPeerConnection peerConnection;
  MediaStreamTrack videoTrack;
  AudioStreamTrack audioTrack;
  Camera cam;
  ParticipantToken participantToken;
}
```

이제 토큰 생성 서비스를 호출하고 토큰을 요청할 비동기 함수를 만들어 보겠습니다. 이 함수는 `WebRTCPublish` 클래스 안에서 정의하겠습니다.

```cs
async Task<StageToken> GetStageToken()
{
  using UnityWebRequest www = new UnityWebRequest("http://localhost:3000/token");
  StageTokenRequest tokenRequest = new StageTokenRequest(
    "[YOUR STAGE ARN]",
    System.Guid.NewGuid().ToString(),
    1440,
    new string[] { "PUBLISH", "SUBSCRIBE" },
    new StageTokenRequestAttributes("ivs-rtx-broadcast-demo")
  );
  www.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.ASCII.GetBytes(JsonUtility.ToJson(tokenRequest)));
  www.downloadHandler = new DownloadHandlerBuffer();
  www.method = UnityWebRequest.kHttpVerbPOST;
  www.SetRequestHeader("Content-Type", "application/json");
  var request = www.SendWebRequest();
  while (!request.isDone)
  {
    await Task.Yield();
  };
  var response = www.downloadHandler.text;
  Debug.Log(response);
  if (www.result != UnityWebRequest.Result.Success)
  {
    Debug.Log(www.error);
    return default;
  }
  else
  {
    StageToken stageToken = StageToken.CreateFromJSON(www.downloadHandler.text);
    Debug.Log(stageToken);
    participantToken = stageToken.participantToken;
    return stageToken;
  }
}
```

`Start()` 메서드 내에서 `WebRTC.Update()` 코루틴을 시작하고, 새 `RTCPeerConnection`을 설정하고, 카메라를 가져와서 그 출력을 `peerConnection`에 추가하고, 게임 오디오도 추가하겠습니다. 그런 다음 잠시 후에 정의할 `DoWhip()`이라는 코루틴을 시작하겠습니다.

```cs
async void Start()
{
  StartCoroutine(WebRTC.Update());
  peerConnection = new RTCPeerConnection
  {
    OnIceConnectionChange = state => { Debug.Log("Peer Connection: " + state); }
  };
  cam = GetComponent<Camera>();
  videoTrack = cam.CaptureStreamTrack(1280, 720);
  peerConnection.AddTrack(videoTrack);
  AudioListener audioListener = cam.GetComponent<AudioListener>();
  audioTrack = new AudioStreamTrack(audioListener) { Loopback = true };
  peerConnection.AddTrack(audioTrack);
  StartCoroutine(DoWHIP());
}
```

토큰을 가져오고, 로컬 SDP를 생성한 다음, 이를 전달하여 원격 SDP를 검색하는 `DoWhip()`을 정의해 보겠습니다 (결국 `peerConnection`에서 이를 설정하여 WebRTC 연결 프로세스를 완료합니다). `Authorization` 헤더에 `Bearer` 값으로 토큰을 전달하여 SDP를 검색하기 위해, Amazon IVS 글로벌 WHIP 엔드포인트(https://global.whip.live-video.net/) 를 사용할 것입니다.

```cs
IEnumerator DoWHIP()
{
  Task getStageTokenTask = GetStageToken();
  yield return new WaitUntil(() => getStageTokenTask.IsCompleted);
  Debug.Log(participantToken.token);
  Debug.Log(participantToken.participantId);

  var offer = peerConnection.CreateOffer();
  yield return offer;

  var offerDesc = offer.Desc;
  var opLocal = peerConnection.SetLocalDescription(ref offerDesc);
  yield return opLocal;

  var filteredSdp = "";
  foreach (string sdpLine in offer.Desc.sdp.Split("\r\n"))
  {
    if (!sdpLine.StartsWith("a=extmap"))
    {
      filteredSdp += sdpLine + "\r\n";
    }
  }
  using (UnityWebRequest www = new UnityWebRequest("https://global.whip.live-video.net/"))
  {
    www.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.ASCII.GetBytes(filteredSdp));
    www.downloadHandler = new DownloadHandlerBuffer();
    www.method = UnityWebRequest.kHttpVerbPOST;
    www.SetRequestHeader("Content-Type", "application/sdp");
    www.SetRequestHeader("Authorization", "Bearer " + participantToken.token);
    yield return www.SendWebRequest();
    if (www.result != UnityWebRequest.Result.Success)
    {
      Debug.Log(www.error);
    }
    else
    {
      var answer = new RTCSessionDescription { type = RTCSdpType.Answer, sdp = www.downloadHandler.text };
      var opRemote = peerConnection.SetRemoteDescription(ref answer);
      yield return opRemote;
      if (opRemote.IsError)
      {
        Debug.Log(opRemote.Error);
      }
    }
  }
}
```

이제 게임을 실행해서 Unity 콘솔에서 토큰 확인 및 연결 설정되었는지 확인할 준비가 되었습니다. Unity에서 `play` 버튼을 눌러 게임을 실행하고 콘솔을 확인합니다.

![Unity console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7clnema487nibglxps1n.png)
 
결과가 괜찮아 보입니다! 토큰이 생성되었고 피어 연결이 `Connected`상태가 되었습니다. 게임플레이가 Stage로 스트리밍되는지 확인해 봅시다.

## 재생 테스트

재생을 테스트하기 위해, [Amazon IVS Web Broadcast SDK](https://aws.github.io/amazon-ivs-web-broadcast/docs/real-time-sdk-guides/introduction)를 사용하여 무대에 연결하고 참가자가 연결될 때 렌더링할 수 있습니다. 이 부분은 오프라인 연습으로 남겨두고 [이 CodePen](https://codepen.io/amazon-ivs/project/editor/ZzWobn)에서 테스트해 보겠습니다. 다른 토큰을 생성하여 클립보드에 복사한 다음, CodePen에 붙여넣고 게임이 실행 중이고 스테이지로 방송을 하는 동안 'Join Stage'를 클릭합니다. 모든 것이 잘 진행되었다면 실시간 방송을 볼 수 있을 것입니다!

{% youtube https://youtu.be/W9IDb_4xRCk %}

> 🔊 **참고:** 위 동영상에서 오디오가 중복되는 것을 볼 수 있는데, 이는 로컬 게임 오디오와 스트림 오디오가 모두 캡처되었기 때문입니다. 

## 리소스 정리

`Destroy()`에서, 'peerConnection'을 닫아서 삭제하고 오디오 및 비디오 트랙리소스들을 정리할 수 있습니다.


```cs
async void OnDestroy()
{
  Debug.Log("OnDestroy");
  if (peerConnection != null)
  {
    peerConnection.Close();
    peerConnection.Dispose();
  }
  if (videoTrack != null) videoTrack.Dispose();
  if (audioTrack != null) audioTrack.Dispose();
}
```

## 어떻게 개선할 수 있을까요?

현재 상태로는 꽤 인상적이라고 말씀드리고 싶습니다! 이 시리즈의 이전 게시물에서 언급한 몇 가지 사항 외에도, 이 시리즈의 다음 게시물에서 다룰 개선 사항이 한 가지 있습니다. 위 동영상에서 이미 눈치채셨을 수도 있습니다. 놓치셨다면 위 동영상에서 실시간 재생을 확인해보시면 스트림에서 경기 타이머와 같은 HUD 요소와 지침과 같은 오버레이가 표시되지 않는 것을 확인할 수 있습니다. 이는 Unity의 캔버스 엘리먼트가 일반적으로 'Screen Space - Overlay'를 사용하도록 구성되어 있어서, 이는 카메라가 게임 화면에 렌더링하는 모든 것 위에 렌더링된다는 것을 의미합니다. 모든 HUD와 UI 요소를 라이브 스트림에 렌더링할 필요가 없을 수도 있으므로(특히 사용자별 데이터를 표시할 수 있는 화면의 경우) 반드시 나쁜 것은 아닙니다. 이는 게임마다 사례별로 처리될 수 있지만, 전체 UI를 렌더링해야 하는 경우에는 이 시리즈의 다음 포스팅에서 이 문제를 해결하는 한 가지 접근 방식을 살펴보겠습니다.

또 다른 개선 사항은 게임이 시작될 때 자동으로 스트림을 실행하는 대신 스트림을 시작/중단할 수 있는 UI 버튼을 추가하는 것입니다.

## 요약

(다소 긴) 이번 포스팅에서는, Unity로 만든 게임에서 Amazon IVS 실시간 Stage로 직접 방송하는 방법에 대해 알아보았습니다. 이 시리즈의 다음 포스팅에서는 반복하지 않을 Stage 생성 및 토큰 생성과 같은 몇 가지 소개 주제를 다루었으므로 복습이 필요한 경우 이 포스팅을 다시 참조하시기 바랍니다.

이 데모에 사용한 전체 스크립트를 보고 싶으시다면 GitHub에서 [이 Gist](https://gist.github.com/recursivecodes/e948b81e53bf9259b981c88e9fc216fa)를 확인하세요. 제 프로덕션 토큰 엔드포인트는 `POST` 메서드를 사용하며 Stage ARN, 사용자 ID 등을 전송할 수 있으므로 이 스크립트는 해당 POST 요청을 모델링하기 위한 몇 가지 추가 클래스가 포함되어 있다는 점에 유의하세요. 여러분의 자체 엔드포인트에서 작동하도록 수정해야 할 수도 있지만 이 스크립트로 시작할 수 있습니다.