ppbbww

Pillar Point boats, birds, and waves watcher
git clone git@abtrout.com:ppbbww.git
Log | Files | Refs | README | LICENSE

stream_sampler.py (2576B)


      1 import aiohttp
      2 import asyncio
      3 import datetime
      4 import logging
      5 import os
      6 import random
      7 import subprocess
      8 import time
      9 
     10 from tempfile import mkstemp
     11 
     12 DEFAULT_HEADERS = {
     13     "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
     14     "Accept": "*/*",
     15     "Accept-Language": "en-US,en;q=0.9",
     16     "Accept-Encoding": "gzip, deflate, br, zstd",
     17     "Origin": "https://www.surfline.com",
     18     "DNT": "1",
     19     "Referer": "https://www.surfline.com/",
     20     "Cache-Control": "no-cache",
     21 }
     22 
     23 class StreamSampler:
     24     def __init__(self, cam_name, data_dir="./data"):
     25         self.base_url = f"https://hls.cdn-surfline.com/oregon/wc-{cam_name}"
     26         self.cam_name = cam_name
     27         self.data_dir = data_dir
     28 
     29     async def get_recent_frames(self):
     30         async with aiohttp.ClientSession(
     31             raise_for_status=True,
     32             skip_auto_headers=list(DEFAULT_HEADERS.keys()),
     33             headers=DEFAULT_HEADERS,
     34         ) as session:
     35             chunk_url = await self.__get_recent_chunk_url(session)
     36             chunk_file = await self.__get_recent_chunk(session, chunk_url)
     37             return self.__extract_frames(chunk_file)
     38 
     39     async def __get_recent_chunk_url(self, session):
     40         url = f"{self.base_url}/playlist.m3u8"
     41         async with session.get(url) as res:
     42             chunklist = await res.text()
     43             # Only check last/final chunk from chunklist.
     44             *_, chunk = [l for l in chunklist.splitlines() if not l.startswith("#")]
     45             return f"{self.base_url}/{chunk}"
     46 
     47     async def __get_recent_chunk(self, session, chunk_url):
     48         async with session.get(chunk_url) as res:
     49             bytes = await res.read()
     50             fd, chunk_file = mkstemp()
     51             with os.fdopen(fd, "wb") as f:
     52                 f.write(bytes)
     53             return chunk_file
     54 
     55     def __extract_frames(self, chunk_file):
     56         dt = datetime.datetime.now()
     57         day, hour, ts = dt.strftime("%m%d%Y"), dt.strftime("%H"), dt.strftime("%s")
     58         frames_dir = f"{self.data_dir}/{self.cam_name}/{day}/{hour}"
     59         if not os.path.exists(frames_dir):
     60             os.makedirs(frames_dir)
     61         subprocess.run(["ffmpeg",
     62             "-hide_banner", "-loglevel", "3",
     63             "-i", chunk_file,
     64             "-vf", "select=eq(pict_type\,I)",
     65             "-vsync", "vfr",
     66             f"{frames_dir}/{ts}-thumb-%04d.jpg"])
     67         os.remove(chunk_file)  # cleanup
     68         # Return filenames of (ffmpeg generated) frames to caller.
     69         return [f for f in os.scandir(frames_dir) if f.name.startswith(f"{ts}-thumb")]