{"id":1030,"date":"2025-05-11T08:50:35","date_gmt":"2025-05-10T23:50:35","guid":{"rendered":"https:\/\/beeknowledge.co.jp\/?p=1030"},"modified":"2025-05-11T08:50:36","modified_gmt":"2025-05-10T23:50:36","slug":"%e7%a0%94%e7%a9%b6%ef%bc%9a%e7%94%bb%e5%83%8fai%e3%83%8d%e3%83%83%e3%83%88%e9%80%9a%e4%bf%a1%e7%92%b0%e5%a2%83%e3%82%92%e8%80%83%e3%81%88%e3%82%8b%ef%bc%88webrtc%ef%bc%89","status":"publish","type":"post","link":"https:\/\/beeknowledge.co.jp\/?p=1030","title":{"rendered":"\u7814\u7a76\uff1a\u753b\u50cfAI\u30cd\u30c3\u30c8\u901a\u4fe1\u74b0\u5883\u3092\u8003\u3048\u308b\uff08WebRTC\uff09"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"ja\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\/>\n  <style>\n    body { font-family: sans-serif; line-height: 1.6; max-width: 800px; margin: auto; padding: 20px; }\n    header { text-align: center; margin-bottom: 40px; }\n    header h1 { font-size: 2.4em; margin: 0; }\n    header p { color: #666; }\n    nav ul { list-style: none; padding: 0; display: flex; gap: 1em; justify-content: center; }\n    nav a { text-decoration: none; color: #007acc; }\n    section { margin-bottom: 40px; }\n    h2 { border-bottom: 2px solid #ddd; padding-bottom: 0.3em; }\n    pre { background: #f5f5f5; padding: 15px; overflow-x: auto; }\n    code { font-family: monospace; }\n    footer { text-align: center; color: #999; margin-top: 60px; font-size: 0.9em; }\n  <\/style>\n<\/head>\n<body>\n\n  <header>\n    <p>2025\u5e745\u670811\u65e5 \uff5c by Reiji Takashide<\/p>\n    <nav>\n      <ul>\n        <li><a href=\"#overview\">\u6982\u8981<\/a><\/li>\n        <li><a href=\"#requirements\">\u8981\u4ef6<\/a><\/li>\n        <li><a href=\"#server\">\u30b5\u30fc\u30d0\u30fc\u5b9f\u88c5<\/a><\/li>\n        <li><a href=\"#client\">\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u5b9f\u88c5<\/a><\/li>\n        <li><a href=\"#tips\">\u904b\u7528\u306e\u30d2\u30f3\u30c8<\/a><\/li>\n      <\/ul>\n    <\/nav>\n  <\/header>\n\n  <article>\n\n    <section id=\"overview\">\n      <h2>\u6982\u8981<\/h2>\n      <p>\n        \u591a\u304f\u306e IP \u30ab\u30e1\u30e9\u306f RTSP \u30d7\u30ed\u30c8\u30b3\u30eb\u3067\u6620\u50cf\u3092\u914d\u4fe1\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u30d6\u30e9\u30a6\u30b6\u306b\u306f\u305d\u306e\u307e\u307e\u518d\u751f\u3067\u304d\u307e\u305b\u3093\u3002\n        \u305d\u3053\u3067 Python \u306e <code>aiortc<\/code> \u30e9\u30a4\u30d6\u30e9\u30ea\u3092\u4f7f\u3044\u3001RTSP \u30b9\u30c8\u30ea\u30fc\u30e0\u3092\u53d7\u3051\u53d6\u308a\u3064\u3064\n        WebRTC \u5f62\u5f0f\u306b\u5909\u63db\u3057\u3001\u30d6\u30e9\u30a6\u30b6\u306b\u30c7\u30ea\u30d0\u30ea\u30fc\u3059\u308b\u30b7\u30f3\u30d7\u30eb\u306a\u30ea\u30ec\u30fc\u30b5\u30fc\u30d0\u3092\u5b9f\u88c5\u3067\u304d\u305f\u3089\u3068\u3044\u3046\u7814\u7a76\u3002\u5b9f\u88c5\u9014\u4e2d\u306a\u306e\u3067\u4f55\u304c\u3042\u3063\u3066\u3082\u8cac\u4efb\u306f\u53d6\u308c\u307e\u305b\u3093\u3002\u60aa\u3057\u304b\u3089\u305a\u3002\n\n      <\/p>\n    <\/section>\n\n    <section id=\"requirements\">\n      <h2>\u8981\u4ef6<\/h2>\n      <ul>\n        <li>Python 3.8 \u4ee5\u4e0a<\/li>\n        <li>\u30e9\u30a4\u30d6\u30e9\u30ea\uff1a<code>aiohttp<\/code>, <code>aiortc<\/code>, <code>opencv-python<\/code><\/li>\n        <li>IP \u30ab\u30e1\u30e9\u306e RTSP URL\uff08\u4f8b\uff1a<code>rtsp:\/\/<em>camera_ip<\/em>\/stream<\/code>\uff09<\/li>\n      <\/ul>\n      <p>\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u30b3\u30de\u30f3\u30c9\uff1a<\/p>\n      <pre><code>pip install aiohttp aiortc opencv-python<\/code><\/pre>\n    <\/section>\n\n    <section id=\"server\">\n      <h2>\u30b5\u30fc\u30d0\u30fc\u5b9f\u88c5 (server.py)<\/h2>\n      <p>RTSP \u304b\u3089\u30d5\u30ec\u30fc\u30e0\u3092\u53d6\u5f97\u3057\u3001WebRTC \u7528\u306b\u5909\u63db\u3057\u3066\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u9001\u51fa\u3057\u307e\u3059\u3002<\/p>\n      <pre><code>import asyncio\nimport cv2\nfrom aiohttp import web\nfrom aiortc import RTCPeerConnection, MediaStreamTrack\n\nRTSP_URL = \"rtsp:\/\/<IP_CAMERA_ADDRESS>\/stream\"\npcs = set()\n\nclass RTSPVideoTrack(MediaStreamTrack):\n    kind = \"video\"\n\n    def __init__(self):\n        super().__init__()\n        self.cap = cv2.VideoCapture(RTSP_URL)\n\n    async def recv(self):\n        pts, time_base = await self.next_timestamp()\n        ret, frame = self.cap.read()\n        if not ret:\n            raise ConnectionError(\"RTSP stream ended\")\n\n        # BGR \u2192 RGB\n        frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n\n        from av import VideoFrame\n        video_frame = VideoFrame.from_ndarray(frame, format=\"rgb24\")\n        video_frame.pts = pts\n        video_frame.time_base = time_base\n        return video_frame\n\nasync def offer(request):\n    params = await request.json()\n    pc = RTCPeerConnection()\n    pcs.add(pc)\n    pc.addTrack(RTSPVideoTrack())\n\n    @pc.on(\"iceconnectionstatechange\")\n    async def on_ice_state():\n        if pc.iceConnectionState == \"failed\":\n            await pc.close()\n            pcs.discard(pc)\n\n    await pc.setRemoteDescription(params[\"sdp\"])\n    answer = await pc.createAnswer()\n    await pc.setLocalDescription(answer)\n\n    return web.json_response({\n        \"sdp\": pc.localDescription.sdp,\n        \"type\": pc.localDescription.type\n    })\n\nasync def on_shutdown(app):\n    coros = [pc.close() for pc in pcs]\n    await asyncio.gather(*coros)\n    pcs.clear()\n\napp = web.Application()\napp.on_shutdown.append(on_shutdown)\napp.router.add_post(\"\/offer\", offer)\n\nif __name__ == \"__main__\":\n    web.run_app(app, port=8080)\n<\/code><\/pre>\n    <\/section>\n\n    <section id=\"client\">\n      <h2>\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u5b9f\u88c5 (index.html)<\/h2>\n      <p>\u30d6\u30e9\u30a6\u30b6\u3067 RTCPeerConnection \u3092\u7acb\u3061\u4e0a\u3052\u3001\u6620\u50cf\u3092\u53d7\u4fe1\u3057\u3066 <code>&lt;video&gt;<\/code> \u8981\u7d20\u306b\u6e21\u3057\u307e\u3059\u3002<\/p>\n      <pre><code>&lt;!DOCTYPE html&gt;\n&lt;html lang=\"ja\"&gt;\n&lt;head&gt;\n  &lt;meta charset=\"UTF-8\" \/&gt;\n  &lt;title&gt;IP \u30ab\u30e1\u30e9 WebRTC \u30d3\u30e5\u30fc\u30a2&lt;\/title&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n  &lt;h1&gt;IP \u30ab\u30e1\u30e9\u6620\u50cf&lt;\/h1&gt;\n  &lt;video id=\"video\" autoplay playsinline controls&gt;&lt;\/video&gt;\n\n  &lt;script&gt;\n    const pc = new RTCPeerConnection();\n    const video = document.getElementById(\"video\");\n\n    pc.ontrack = ({ track, streams }) =&gt; {\n      if (track.kind === \"video\") {\n        video.srcObject = streams[0];\n      }\n    };\n\n    async function start() {\n      const offer = await pc.createOffer();\n      await pc.setLocalDescription(offer);\n\n      const res = await fetch(\"\/offer\", {\n        method: \"POST\",\n        headers: { \"Content-Type\": \"application\/json\" },\n        body: JSON.stringify({ sdp: pc.localDescription })\n      });\n      const answer = await res.json();\n      await pc.setRemoteDescription(answer);\n    }\n\n    start().catch(console.error);\n  &lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;\n<\/code><\/pre>\n    <\/section>\n\n    <section id=\"tips\">\n      <h2>\u904b\u7528\u306e\u30d2\u30f3\u30c8<\/h2>\n      <ul>\n        <li>\u4f4e\u9045\u5ef6\u3092\u91cd\u8996\u3059\u308b\u5834\u5408\u306f\u3001GStreamer\uff0b<code>webrtcbin<\/code> \u3084\u5c02\u7528\u30e1\u30c7\u30a3\u30a2\u30b5\u30fc\u30d0\uff08Janus, Kurento\uff09\u3092\u691c\u8a0e\u3002<\/li>\n        <li>NAT \u8d8a\u3048\u306e\u305f\u3081\u306b STUN\/TURN \u30b5\u30fc\u30d0\u3092\u4f75\u7528\u3059\u308b\u3068\u5b89\u5b9a\u6027\u304c\u5411\u4e0a\u3002<\/li>\n        <li>IP \u30ab\u30e1\u30e9\u8a8d\u8a3c\u60c5\u5831\u306f\u74b0\u5883\u5909\u6570\u3084\u5b89\u5168\u306a\u8a2d\u5b9a\u30d5\u30a1\u30a4\u30eb\u3067\u7ba1\u7406\u3002<\/li>\n      <\/ul>\n    <\/section>\n\n  <\/article>\n\n  <footer>\n    &copy; 2025 Reiji Takashide. All rights reserved.\n  <\/footer>\n\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>2025\u5e745\u670811\u65e5 \uff5c by Reiji Takashide \u6982\u8981 \u8981\u4ef6 \u30b5\u30fc\u30d0\u30fc\u5b9f\u88c5 \u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u5b9f\u88c5 \u904b\u7528\u306e\u30d2\u30f3\u30c8 \u6982\u8981 \u591a\u304f\u306e IP \u30ab\u30e1\u30e9\u306f RTSP \u30d7\u30ed\u30c8\u30b3\u30eb\u3067\u6620\u50cf\u3092\u914d\u4fe1\u3057\u3066\u3044\u307e\u3059\u304c\u3001\u30d6\u30e9\u30a6\u30b6\u306b\u306f\u305d\u306e\u307e\u307e\u518d [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"saved_in_kubio":false,"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"sns_share_botton_hide":"","vkExUnit_sns_title":"","_vk_print_noindex":"","sitemap_hide":"","vkExUnit_EyeCatch_disable":"","_veu_custom_css":"","veu_display_promotion_alert":"common","vkexunit_cta_each_option":"","_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[6],"tags":[],"class_list":["post-1030","post","type-post","status-publish","format-standard","hentry","category-programing"],"aioseo_notices":[],"veu_head_title_object":{"title":"","add_site_title":""},"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=\/wp\/v2\/posts\/1030","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1030"}],"version-history":[{"count":1,"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=\/wp\/v2\/posts\/1030\/revisions"}],"predecessor-version":[{"id":1031,"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=\/wp\/v2\/posts\/1030\/revisions\/1031"}],"wp:attachment":[{"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1030"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1030"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/beeknowledge.co.jp\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1030"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}