Ant Media Server + RTMP 영상 재생
Setting
설치
Dockerfile과 docker-compose 파일을 다운받는다
1
2
wget https://raw.githubusercontent.com/ant-media/Scripts/master/docker/docker-compose.yml
wget https://raw.githubusercontent.com/ant-media/Scripts/master/docker/Dockerfile_Process -O Dockerfile
github에서 community 버전 zip 파일을 다운받는다
https://github.com/ant-media/Ant-Media-Server/releases
docker compose 설정
- args
- AntMediaServer : github 에서 다운받은 zip 이름
- windows 에서는 아직
network_mode: host
이 지원이 되지 않아서 port를 설정해주었다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
services:
antmedia:
build:
context: ./
dockerfile: ./Dockerfile
args:
AntMediaServer: "ant-media-server-community-2.13.2.zip"
ports:
- "5080:5080" # HTTP
- "5443:5443" # HTTPS
- "1935:1935" # RTMP
container_name: antmedia
restart: always
entrypoint: /usr/local/antmedia/start.sh
# network_mode: host
# volumes:
# - antmedia_vol:/usr/local/antmedia/
#volumes:
# antmedia_vol:
# external: true
# name:
# antmedia_volume
명령어를 실행한다
1
2
docker-compose build
docker-compose up -d
http://localhost:5080/ 에 접속해서 Ant Media Server 관리 화면이 나오는지 확인한다
계정을 만들고 로그인을 하면 대시보드를 확인할 수 있다
Streaming Test
OBS Studio 송출
OBS Studio설정에서 방송 → 서비스 : 사용자 지정 → 서버와 스트림 키 입력
스트림 키는 자신이 원하는 것으로 입력하면 된다
대시보드를 확인하면 Active Live Streams 가 1로 추가된것을 확인할 수 있다
영상 확인
왼쪽 메뉴의 APPLICATIONS에서 LiveAPP 클릭하면 Stream 목록을 확인할 수 있다
Ant Media Server 설정
Community 에서는 영상 저장, api security, 푸쉬 알람 등이 설정 가능하다
자동 영상 저장 설정
obs에서 방송을 종료하면 VoD에 저장된것을 확인할 수 있다
S3계정이 있으면 S3 에도 저장이 가능하다
API Security
JWT, IP 화이트리스트 등을 통해 보안 설정이 가능하다
지금은 원할한 영상 재생을 위해 보안을 해제했다
API 통신 테스트
https://antmedia.io/rest/ 에서 api 목록을 확인할 수 있다
- 현재 방송 중인 목록 가져오기
Request
1
curl -X 'GET' 'http://localhost:5080/LiveApp/rest/v2/broadcasts/list/0/10?type_by=liveStream' -H 'accept: application/json'
Response
1
[{"streamId":"test","status":"broadcasting","playListStatus":null,"type":"liveStream","publishType":"RTMP","name":null,"description":null,"publish":true,"date":1747187095739,"plannedStartDate":0,"plannedEndDate":0,"duration":60054,"endPointList":null,"playListItemList":null,"publicStream":true,"is360":false,"listenerHookURL":null,"category":null,"ipAddr":null,"username":null,"password":null,"quality":null,"speed":0.997,"streamUrl":null,"originAdress":"172.21.0.2","mp4Enabled":1,"webMEnabled":0,"seekTimeInMs":0,"conferenceMode":null,"subtracksLimit":-1,"expireDurationMS":0,"rtmpURL":"rtmp://172.21.0.2/LiveApp/test","zombi":true,"pendingPacketSize":1,"hlsViewerCount":0,"dashViewerCount":0,"webRTCViewerCount":0,"rtmpViewerCount":0,"startTime":1747187096837,"receivedBytes":18774200,"bitrate":2503224,"width":1280,"height":720,"encoderQueueSize":0,"dropPacketCountInIngestion":0,"dropFrameCountInEncoding":0,"packetLostRatio":0.0,"packetsLost":0,"jitterMs":0,"rttMs":0,"userAgent":"N/A","remoteIp":null,"latitude":null,"longitude":null,"altitude":null,"mainTrackStreamId":"","subTrackStreamIds":[],"absoluteStartTimeMs":0,"webRTCViewerLimit":-1,"hlsViewerLimit":-1,"dashViewerLimit":-1,"subFolder":null,"currentPlayIndex":0,"metaData":"","playlistLoopEnabled":true,"updateTime":1747187156891,"role":"","hlsParameters":null,"autoStartStopEnabled":false,"encoderSettingsList":null,"virtual":false,"anyoneWatching":false}]
방송 화면 표시하기
실시간 영상 확인
- html
broadcasts/list/0/10?type_by=liveStream
: StreamId를 가져온다- StreamId를 포함한 src를 구성해서 frame에서 재생한다
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Ant Media Server - 방송 목록</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; margin: 0; padding: 20px; background-color: #f4f7f6; color: #333; } .main-title { text-align: center; color: #2c3e50; margin-bottom: 30px; } .section { background-color: #fff; padding: 20px; margin-bottom: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .section-title { border-bottom: 2px solid #3498db; padding-bottom: 10px; margin-top: 0; margin-bottom: 20px; color: #3498db; font-size: 1.8em; } .item-container { margin-bottom: 25px; padding: 15px; border: 1px solid #ecf0f1; border-radius: 5px; background-color: #fdfdfd; } .item-container h3 { margin-top: 0; margin-bottom: 10px; color: #333; font-size: 1.4em; } .item-container iframe { width: 100%; max-width: 640px; height: auto; /* 자동 높이 조절, width에 맞춰 비율 유지 */ aspect-ratio: 16 / 9; /* 16:9 비율 유지 */ border: none; border-radius: 4px; background-color: #000; /* 비디오 로딩 중 배경색 */ } .item-details { font-size: 0.85em; color: #555; margin-bottom: 10px; line-height: 1.6; } .item-details span { margin-right: 15px; } .loading-message, .error-message { text-align: center; font-size: 1.1em; color: #7f8c8d; padding: 20px; } .error-message { color: #e74c3c; } .no-content { text-align: center; color: #7f8c8d; padding: 20px; font-style: italic; } </style> </head> <body> <h1 class="main-title">Ant Media Server 연동 페이지</h1> <div class="section"> <h2 class="section-title">실시간 방송 목록</h2> <div id="loadingMessageLive" class="loading-message"> 실시간 방송 목록을 불러오는 중... </div> <div id="streamsListContainer"></div> </div> <script> // --- 설정 (사용자 환경에 맞게 수정해주세요) --- const antMediaServerUrl = "http://localhost:5080"; // Ant Media Server 주소 const applicationName = "LiveApp"; // 사용하는 애플리케이션 이름 // ----------------------------------------- // 헬퍼 함수: 밀리초를 MM:SS 또는 HH:MM:SS 형식으로 변환 function formatDuration(ms) { if (ms === null || typeof ms === "undefined" || ms < 0) return "N/A"; let seconds = Math.floor(ms / 1000); let minutes = Math.floor(seconds / 60); let hours = Math.floor(minutes / 60); seconds %= 60; minutes %= 60; const pad = (num) => String(num).padStart(2, "0"); if (hours > 0) { return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; } return `${pad(minutes)}:${pad(seconds)}`; } // 헬퍼 함수: 타임스탬프를 YYYY-MM-DD HH:MM:SS 형식으로 변환 function formatTimestamp(ts) { if (ts === null || typeof ts === "undefined" || ts <= 0) return "N/A"; const date = new Date(ts); const pad = (num) => String(num).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad( date.getDate() )} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad( date.getSeconds() )}`; } // 실시간 방송 목록 가져와서 표시하는 함수 async function fetchAndDisplayLiveStreams() { const streamsContainer = document.getElementById( "streamsListContainer" ); const loadingMessage = document.getElementById("loadingMessageLive"); streamsContainer.innerHTML = ""; // 기존 목록 초기화 loadingMessage.style.display = "block"; try { const response = await fetch( `${antMediaServerUrl}/${applicationName}/rest/v2/broadcasts/list/0/10?type_by=liveStream`, { method: "GET", headers: { Accept: "application/json" }, } ); if (!response.ok) { throw new Error( `API 호출 실패 (Live): ${response.status} ${response.statusText}. 서버 응답을 확인하세요.` ); } const broadcasts = await response.json(); const liveStreams = broadcasts.filter( (stream) => stream.status === "broadcasting" ); loadingMessage.style.display = "none"; if (liveStreams.length === 0) { streamsContainer.innerHTML = '<p class="no-content">현재 진행 중인 실시간 방송이 없습니다.</p>'; return; } liveStreams.forEach((stream) => { const streamId = stream.streamId; const streamName = stream.name || streamId; // 스트림 이름이 없으면 ID 사용 const streamDiv = document.createElement("div"); streamDiv.className = "item-container"; // 공통 클래스 사용 streamDiv.innerHTML = `<h3>${streamName}</h3>`; const iframeSrc = `${antMediaServerUrl}/${applicationName}/play.html?name=${streamId}&autoplay=true&mute=true&playType=webrtc,hls`; // WebRTC 우선, HLS fallback const iframeElement = document.createElement("iframe"); iframeElement.setAttribute("src", iframeSrc); iframeElement.setAttribute("frameborder", "0"); iframeElement.setAttribute("allowfullscreen", ""); iframeElement.setAttribute( "allow", "autoplay; fullscreen;encrypted-media;picture-in-picture" ); streamDiv.appendChild(iframeElement); streamsContainer.appendChild(streamDiv); }); } catch (error) { console.error("실시간 방송 정보 로딩 오류:", error); loadingMessage.style.display = "none"; streamsContainer.innerHTML = `<p class="error-message">실시간 방송 목록 로딩 오류: ${error.message}<br>CORS 설정 및 Ant Media Server 상태를 확인하세요.</p>`; } } // 페이지 로드 시 모든 함수 실행 document.addEventListener("DOMContentLoaded", () => { fetchAndDisplayLiveStreams(); fetchAndDisplayVoDs(); }); // (선택 사항) 주기적으로 실시간 방송 목록 갱신 // setInterval(fetchAndDisplayLiveStreams, 30000); // 30초마다 </script> </body> </html>
결과
저장된 영상 확인
- html
vods/list/0/20?sort_by=date&order_by=desc
: VoD 객체 목록을 가져온다- VoD 객체에서 경로를 가져와서 vide에서 재생한다
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Ant Media Server - 방송 및 VoD 목록</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; margin: 0; padding: 20px; background-color: #f4f7f6; color: #333; } .main-title { text-align: center; color: #2c3e50; margin-bottom: 30px; } .section { background-color: #fff; padding: 20px; margin-bottom: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .section-title { border-bottom: 2px solid #3498db; padding-bottom: 10px; margin-top: 0; margin-bottom: 20px; color: #3498db; font-size: 1.8em; } .item-container { /* stream-container, vod-container 공통 스타일 */ margin-bottom: 25px; padding: 15px; border: 1px solid #ecf0f1; border-radius: 5px; background-color: #fdfdfd; } .item-container h3 { margin-top: 0; margin-bottom: 10px; color: #333; font-size: 1.4em; } .item-container iframe, .item-container video { width: 100%; max-width: 640px; height: auto; /* 자동 높이 조절, width에 맞춰 비율 유지 */ aspect-ratio: 16 / 9; /* 16:9 비율 유지 */ border: none; border-radius: 4px; background-color: #000; /* 비디오 로딩 중 배경색 */ } .item-details { font-size: 0.85em; color: #555; margin-bottom: 10px; line-height: 1.6; } .item-details span { margin-right: 15px; } .loading-message, .error-message { text-align: center; font-size: 1.1em; color: #7f8c8d; padding: 20px; } .error-message { color: #e74c3c; } .no-content { text-align: center; color: #7f8c8d; padding: 20px; font-style: italic; } </style> </head> <body> <h1 class="main-title">Ant Media Server 연동 페이지</h1> <div class="section"> <h2 class="section-title">실시간 방송 목록</h2> <div id="loadingMessageLive" class="loading-message"> 실시간 방송 목록을 불러오는 중... </div> <div id="streamsListContainer"></div> </div> <div class="section"> <h2 class="section-title">저장된 영상 (VoD)</h2> <div id="loadingMessageVoD" class="loading-message"> 저장된 영상 목록을 불러오는 중... </div> <div id="vodsListContainer"></div> </div> <script> // --- 설정 (사용자 환경에 맞게 수정해주세요) --- const antMediaServerUrl = "http://localhost:5080"; // Ant Media Server 주소 const applicationName = "LiveApp"; // 사용하는 애플리케이션 이름 // ----------------------------------------- // 헬퍼 함수: 밀리초를 MM:SS 또는 HH:MM:SS 형식으로 변환 function formatDuration(ms) { if (ms === null || typeof ms === "undefined" || ms < 0) return "N/A"; let seconds = Math.floor(ms / 1000); let minutes = Math.floor(seconds / 60); let hours = Math.floor(minutes / 60); seconds %= 60; minutes %= 60; const pad = (num) => String(num).padStart(2, "0"); if (hours > 0) { return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; } return `${pad(minutes)}:${pad(seconds)}`; } // 헬퍼 함수: 타임스탬프를 YYYY-MM-DD HH:MM:SS 형식으로 변환 function formatTimestamp(ts) { if (ts === null || typeof ts === "undefined" || ts <= 0) return "N/A"; const date = new Date(ts); const pad = (num) => String(num).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad( date.getDate() )} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad( date.getSeconds() )}`; } // 실시간 방송 목록 가져와서 표시하는 함수 async function fetchAndDisplayLiveStreams() { const streamsContainer = document.getElementById( "streamsListContainer" ); const loadingMessage = document.getElementById("loadingMessageLive"); streamsContainer.innerHTML = ""; // 기존 목록 초기화 loadingMessage.style.display = "block"; try { const response = await fetch( `${antMediaServerUrl}/${applicationName}/rest/v2/broadcasts/list/0/10?type_by=liveStream`, { method: "GET", headers: { Accept: "application/json" }, } ); if (!response.ok) { throw new Error( `API 호출 실패 (Live): ${response.status} ${response.statusText}. 서버 응답을 확인하세요.` ); } const broadcasts = await response.json(); const liveStreams = broadcasts.filter( (stream) => stream.status === "broadcasting" ); loadingMessage.style.display = "none"; if (liveStreams.length === 0) { streamsContainer.innerHTML = '<p class="no-content">현재 진행 중인 실시간 방송이 없습니다.</p>'; return; } liveStreams.forEach((stream) => { const streamId = stream.streamId; const streamName = stream.name || streamId; // 스트림 이름이 없으면 ID 사용 const streamDiv = document.createElement("div"); streamDiv.className = "item-container"; // 공통 클래스 사용 streamDiv.innerHTML = `<h3>${streamName}</h3>`; const iframeSrc = `${antMediaServerUrl}/${applicationName}/play.html?name=${streamId}&autoplay=true&mute=true&playType=webrtc,hls`; // WebRTC 우선, HLS fallback const iframeElement = document.createElement("iframe"); iframeElement.setAttribute("src", iframeSrc); iframeElement.setAttribute("frameborder", "0"); iframeElement.setAttribute("allowfullscreen", ""); iframeElement.setAttribute( "allow", "autoplay; fullscreen;encrypted-media;picture-in-picture" ); streamDiv.appendChild(iframeElement); streamsContainer.appendChild(streamDiv); }); } catch (error) { console.error("실시간 방송 정보 로딩 오류:", error); loadingMessage.style.display = "none"; streamsContainer.innerHTML = `<p class="error-message">실시간 방송 목록 로딩 오류: ${error.message}<br>CORS 설정 및 Ant Media Server 상태를 확인하세요.</p>`; } } // 저장된 영상(VoD) 목록 가져와서 표시하는 함수 async function fetchAndDisplayVoDs() { const vodsContainer = document.getElementById("vodsListContainer"); const loadingMessage = document.getElementById("loadingMessageVoD"); vodsContainer.innerHTML = ""; loadingMessage.style.display = "block"; try { const response = await fetch( `${antMediaServerUrl}/${applicationName}/rest/v2/vods/list/0/20?sort_by=date&order_by=desc`, { // 최근 20개, 최신순 정렬 method: "GET", headers: { Accept: "application/json" }, } ); if (!response.ok) { throw new Error( `API 호출 실패 (VoD): ${response.status} ${response.statusText}. 서버 응답을 확인하세요.` ); } const vods = await response.json(); loadingMessage.style.display = "none"; if (!vods || vods.length === 0) { vodsContainer.innerHTML = '<p class="no-content">저장된 영상이 없습니다.</p>'; return; } vods.forEach((vod) => { const vodId = vod.vodId; const vodDisplayName = vod.vodName || vodId; const vodFileName = vod.vodName; // API 응답의 vodName이 실제 파일명 (예: "my_video.mp4")이라고 가정 if (!vodFileName) { // vodName이 없는 경우 건너뛰거나 다른 ID 사용 console.warn("VoD 이름이 없어 건너뜁니다:", vod); return; } const creationDateFormatted = formatTimestamp(vod.creationDate); const durationFormatted = formatDuration(vod.duration); const fileSizeMB = vod.fileSize ? (vod.fileSize / (1024 * 1024)).toFixed(2) + " MB" : "N/A"; const vodDiv = document.createElement("div"); vodDiv.className = "item-container"; // 공통 클래스 사용 vodDiv.innerHTML = ` <h3>${vodDisplayName}</h3> <div class="item-details"> <span>ID: ${vodId}</span> <span>생성일: ${creationDateFormatted}</span> <span>길이: ${durationFormatted}</span> <span>크기: ${fileSizeMB}</span> </div> `; // HTML5 <video> 태그 사용 const videoSrc = `${antMediaServerUrl}/${applicationName}/streams/${vodFileName}`; const videoElement = document.createElement("video"); videoElement.setAttribute("controls", ""); videoElement.setAttribute("preload", "metadata"); // 메타데이터만 미리 로드 videoElement.setAttribute("src", videoSrc); videoElement.addEventListener("error", function (e) { console.error(`VoD '${vodDisplayName}' 재생 중 오류:`, e); const errorMsgElement = document.createElement("p"); errorMsgElement.className = "error-message"; errorMsgElement.style.textAlign = "left"; errorMsgElement.textContent = "영상을 불러오는 데 실패했습니다. 파일이 서버에 정확한 경로로 존재하고 접근 가능한지 확인해주세요."; if (videoElement.parentNode) { videoElement.parentNode.insertBefore( errorMsgElement, videoElement.nextSibling ); } else { vodDiv.appendChild(errorMsgElement); } videoElement.style.display = "none"; }); vodDiv.appendChild(videoElement); vodsContainer.appendChild(vodDiv); }); } catch (error) { console.error("저장된 영상 정보 로딩 오류:", error); loadingMessage.style.display = "none"; vodsContainer.innerHTML = `<p class="error-message">저장된 영상 목록 로딩 오류: ${error.message}<br>CORS 설정 및 Ant Media Server 상태를 확인하세요.</p>`; } } // 페이지 로드 시 모든 함수 실행 document.addEventListener("DOMContentLoaded", () => { fetchAndDisplayLiveStreams(); fetchAndDisplayVoDs(); }); // (선택 사항) 주기적으로 실시간 방송 목록 갱신 // setInterval(fetchAndDisplayLiveStreams, 30000); // 30초마다 </script> </body> </html>
결과
Docker Volume Mount
현재 설정으로는 Docker Container가 종료 또는 삭제되면 영상을 확인할 수 없다
Volume Mount
를 통해 데이터를 보존해야한다
docker volume 생성
1
docker volume create antmedia_volume
volumes 주석 해제
docker compose를 다시 실행해준다
1 2 3
docker compose down docker compose build docker compose up -d