
Streamowanie z Raspberry Pi do YouTube’a ale tylko wtedy gdy ktoś ogląda [Out Of the box Project]
Wstęp
W tym projekcie pokażę jak streamować obraz z kamery USB podpiętej do Raspberry Pi do YouTube’a. Streamowanie nie będzie ciągłe – kamera będzie włączona i streamowała do YouTube’a tylko wtedy, gdy ktoś otworzy stronę z oknem YouTube’a.
Co będzie potrzebne
- Kamera USB – ja używam modelu A4Tech PK-910H – przed kupnem kamery należy sprawdzić czy będzie ona działała z Rasbianem
- Raspberry Pi – ja używam RPI4 z 4GB Ramu, testowałem też na RPI3(poniżej screen z Rpi4
htop
przy streamowaniu obrazu o rozdzielczości1280x720
jak widać rdzenie procesora są wykorzystane w 36% na jednym rdzeniu)
-
- Radiator na Procesor Raspberry Pi – streaming potrafi nieźle “zmęczyć” Raspberry Pi dlatego radiator jest potrzebny, ja używam Flirc Raspberry Pi 4 Case (Silver)
jest to aluminiowa obudowa która pełni jednocześnie role radiatora. Przy jej wykorzystaniu temperatura procesora to około 45C przy temperaturze zewnętrznej 24C . Więcej o tej obudowanie na filmie Pana Andreas Spiessa jedyne zastrzeżenie jakie mam do tej obudowy to takie, że brak jest wyprowadzenia na taśmę od kamery podpiętej bezpośrednio w płytkę RPI, oraz to aby wyprowadzić PINY taśmą 40pinową potrzebne jest spiłowanie boków wtyczki taśmy
Jak to będzie działać
Aby rozpocząć stream będzie uruchamiana komenda:
ffmpeg -threads 0 -f v4l2 -i /dev/video0 -ar 44100 -ac 2 -acodec pcm_s16le -f s16le -ac 2 -i /dev/zero -acodec aac -ab 128k -strict experimental -s 1280x720 -b 6000000 -aspect 16:9 -vcodec h264_omx -vb 820k -pix_fmt yuv420p -g 60 -r 30 -f flv rtmp://a.rtmp.youtube.com/live2/SECRET_KEY
oczywiście wcześniej trzeba zainstalować ffmpeg
jeżeli nie mamy (sudo apt-get install ffmpeg )
skrypt ten będzie uruchamiany gdy nasza strona internetowa z ramką z YouTube’a będzie wyświetlana przez co najmniej jedną osobę, gdy strona zostanie zamknięta i nie zostanie ponownie otworzona w przeciągu 10 sekund skrypt zostanie ‘ubity’ kamerka się wyłączy i nie będzie streamować do YouTube’a.
Ponieważ tak naprawdę będziemy tworzyć za każdym razem nowy stream, iframe z adresem streamu musi wskazywać nie na określony stream tylko na aktywny stream danego użytkownika czyli:
https://www.youtube.com/embed/live_stream?channel=CHANNEL_ID&autoplay=1
Wymusza to też żeby nasze streamy były publiczne
Do dzieła 🙂
tak naprawdę żeby uruchomić projekt wystarczy parę klików – ponieważ ten projekt jest już w zakładce quick projects (https://app.remoteme.org/en/#/app/quickstart)
klikamy “read more or build it” przy projekcie:
następnie w zakładce Build It
:
uzupełniamy YouTube stream name/key, który pobieramy z YouTube’a pod adresem https://www.youtube.com/live_dashboard
będzie on miał postać podobną do : 7awp-w65c-zbas-61c9 -
możecie jednak wpisać cokolwiek i potem uzupełnić na prawidłowy secret key w pliku youtuberun.sh
Gdy wcześniej nic nie streamowaliśmy musimy aktywować usługę – aktywacja trwa do 24h dopiero wtedy będziemy mogli zacząć streaming
Channel id pobieramy z adresu https://www.youtube.com/account_advanced
w moim przypadku to ZCPuP7AKof_poZY664N4yJCA
po uzupełnieniu
następnie klikamy Next step – wybieramy albo dodajemy Raspberry Pi ( w razie problemów odsyłam do artykułu o RaspberryPi w RemoteMei o urządzeniach pythonowych w RemoteMe ) , następnie next step i zielony przycisk “build the project” i po poprawnym utworzeniu projektu zamykamy okno.
Co się stało
Została utworzona strona , dodany skrypt pythonowy:
stronę możemy otworzyć, żeby zobaczyć czy wszystko działa:
po otwarciu zobaczymy stronę gdzie po chwili pojawi się obraz ze streamingu.
Jak to dokładnie działa (i co zrobić jak obraz się nie wyświetla)
Zacznijmy od omówienia plików w urządzeniu python script
:
1) youtuberun.sh – po otwarciu widzimy polecenie ffmpeg:
ffmpeg -threads 0 -f v4l2 -i /dev/video0 -ar 44100 -ac 2 -acodec pcm_s16le -f s16le -ac 2 -i /dev/zero -acodec aac -ab 128k -strict experimental -s 1280x720 -b 6000000 -aspect 16:9 -vcodec h264_omx -vb 820k -pix_fmt yuv420p -g 60 -r 30 -f flv rtmp://a.rtmp.youtube.com/live2/SECRET_KEY
jak obraz nie streamuje się do YouTube polecam uruchomić to polecenie bezpośrednio w konsoli i sprawdzić logi może się okazać, że nie mamy zainstalowanego ffmpeg
(wtedy instalujemy poprzez wywołanie sudo apt-get install ffmpeg
) albo nasza kamera jest pod innym adresem niż /dev/video0
( wtedy edytujemy plik i zapisujemy ), w pliku tym możemy też zmienić parametry naszego strumienia wysyłanego do YouTube – podgląd streama znajduje się pod adresem : https://www.youtube.com/live_dashboard
2) python.py jest bardziej skomplikowany – żeby zrozumieć jak działa pamiętajcie, że nasza strona internetowa ma id 2:
oraz, że wartość zmiennej youtube_state (o zmiennych w RemoteMe ) przetrzymuje aktualny stan streamu YouTube’a i jest to:
-1:Youtube FAIL
0:Youtube OFF
1: Youtube ON
2:Youtube STARTING
3:Youtube CLOSING
omówię teraz poszczególne części kodu pliku python.py
1 2 3 4 5 6 7 8 9 10 11 12 |
def onDeviceConnectionChange(deviceId,state): global receiveDeviceConnected if deviceId == 2: if state: logger.info("device receive is now ON ") else: logger.info("device receive is now OFF transmission will end in 10s ") receiveDeviceConnected = state #w funkcji inicializującej remoteMe.subscribeDeviceConnectionChangeEvent(onDeviceConnectionChange) |
onDeviceConnectionChange
do wywołania jak jakiekolwiek urządzenie zmieni stan połączenia z platformą RemoteMe – a w samej funkcji filtrujemy tylko zmiany statusu dla urządzenia o id 2 (bo to deviceId naszej strony internetowej ), następnie aktualny stan zapisujemy do zmiennej globalnej receiveDeviceConnected
odczyt stanu zmiennej następuje w osobnym wątku którego kod wygląda następująco:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def youtubeThreadFunction(): global receiveDeviceConnected global youtubeState currentOn = False wait=0 while True: logger.info('receiveDeviceConnected {} currentOn {} wait {} youtube state : {}'.format(receiveDeviceConnected,currentOn,wait,youtubeState)) if not currentOn and receiveDeviceConnected: currentOn=True startYoutube() wait=0 elif currentOn and not receiveDeviceConnected: if wait > 10: wait= 0 currentOn = False endYoutube() else: wait=wait+1 elif currentOn and youtubeState == -1: currentOn = False endYoutube() wait= 0 time.sleep(1) |
funkcja ma nieskończona pętle która sprawdza czy urządzenie jest podłączone i w zależności jaki był poprzedni stan włącza streaming albo po 10 sekundach od odłączenia strony internetowej zamyka streaming.
Streaming włącza funkcja
1 2 3 4 5 6 7 8 |
def startYoutube(): global thread1,process #thread1.stop() onYoutube(2) process = subprocess.Popen(['bash', 'youtuberun.sh'], stdout=subprocess.PIPE,stderr=subprocess.PIPE) thread1 = threading.Thread(target = checkingThread) thread1.start() logger.info("process ID {}".format(process.pid)) |
1 2 3 4 5 6 7 8 |
def endYoutube(): global process for x in range(0, 3): subprocess.run(["pkill","-TERM","-P",format(process.pid)]) logger.info("pkill -TERM -P {}".format(process.pid)) time.sleep(0.5) onYoutube(0) |
dodatkowo obie funkcje ustawiają zmienne youtube_state poprzez wywołanie funkcji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
def onYoutube(b): global youtubeState youtubeState = b remoteMe.getVariables().setInteger("youtube_state", b) if b==1: logger.info('Youtube ON') elif b==0: logger.info('Youtube OFF') elif b==-1: logger.info('Youtube FAIL') elif b==2: logger.info('Youtube STARTING') elif b==3: logger.info('Youtube CLOSING') else: logger.info('Youtube undefined') |
wartość zmiennej youtube_state jest odczytywana przez stronę internetową (omówione poniżej) i wyświetla odpowiedni komunikat użytkownikowi – wstrzykuje kod iframe’a z podglądem YouTube.
funkcją wartą omówienia jest też funkcja:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
def checkingThread(): global process i=0 while True: time.sleep(0.2) line = process.stderr.read(200) i=i+1 if line and 'speed=' in line.decode("utf-8"): onYoutube(1) return if i>100: onYoutube(-1) return |
sprawdza ona poprzez skanowanie wyjścia skryptu youtuberun.sh czy pojawił się napis “speed=” w przeciągu 40s jeżeli tak oznacza to że nasz ffmpeg poprawnie nadaje streaming, jeżeli nie oznacza to, że coś poszło nie tak i ustawiana jest odpowiednia wartość zmiennej youtube_state
Dodatkowo python.py sprawdza temperaturę naszej malinki i ustawia zmienna cpu_temp która jest wyświetlana na stronie z transmisją.
Co się dzieje w stronie internetowej
tutaj znajdziecie dokładny opis czym jest strona internetowa w RemoteMe, a tutaj dodane komponenty RemoteMe
a dokładniej w pliky script.js bo jedynie on wymaga objaśnień:
w części:
1 2 3 4 5 6 7 8 9 10 11 |
remoteme.remoteMeConfig.deviceConnectionChange.push((deviceId, connected) => { if (deviceId == 1) {//rpi device clearTimeout(notConnected); if (!connected) { showInfoModal("Raspberry Pi Not Connected - You cannot interact ", "fas fa-unlink", "#FF0000"); } else { showInfoModal("Raspberry Pi connected", "fab fa-raspberry-pi", "#00c900", 2); connectYoutubeVariable(); } } }); |
sprawdzamy czy nasze RPI jest podłączone jeżeli tak wywołujemy funkcję
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 |
function connectYoutubeVariable() { remoteme = RemoteMe.getInstance(); remoteme.getVariables().observeInteger("youtube_state", (state) => { let htmlYoutubeState = ""; if (state == 0) { htmlYoutubeState=`<i class="fab fa-youtube" style='color:black'></i> not connected`; } else if (state == 1) { htmlYoutubeState=`<i class="fab fa-youtube" style='color:#00c900'></i>connected`; if (previousYoutube != 1) { if (previousYoutube != undefined) { showInfoModal("Youtube actived. Page will be reload in 10s", "fab fa-youtube", "#00c900"); setTimeout(() => { location.reload(true); }, 10000); } else { $("#youtubeContainer").html(`<iframe width="100%" height="100%" src="https://www.youtube.com/embed/live_stream?channel=CHANELID&autoplay=1" frameborder="0" allowfullscreen></iframe>`); showInfoModal("Youtube actived.", "fab fa-youtube", "#00c900", 2); } } } else if (state == -1) { showInfoModal("Youtube connect fail", "fab fa-youtube", "#00c900"); htmlYoutubeState=`<i class="fab fa-youtube" style='color:#00c900'></i>connected`; } else if (state == 2) { showInfoModal("Youtube starting", "fab fa-youtube", "#0000FF"); htmlYoutubeState=`<i class="fab fa-youtube" style='color:#0000FF'></i>connecting`; } else if (state == 3) { showInfoModal("Youtube closing", "fab fa-youtube", "orange"); htmlYoutubeState=`<i class="fab fa-youtube" style='color:orange'></i>closing`; } previousYoutube = state; $('#youtubeStatediv').html(htmlYoutubeState); }); } |
funkcja ta wyświetla użytkownikowi powiadomienia, oraz gdy YouTube jest aktywnie włączony wstrzykuje ramkę YouTube’a do htmla:
$("#youtubeContainer").html(<iframe width="100%" height="100%" src="https://www.youtube.com/embed/live_stream?channel=CHANELID&autoplay=1" frameborder="0" allowfullscreen></iframe>
);
dzieje się to jednak tylko wtedy gdy aktywny status transmisji jest pierwszym otrzymanym – w przeciwnym razie odświeżamy stronę po 10 sekundach ( z niewiadomych mi przyczyn takie rozwiązanie okazuje się lepsze niż po prostu wstrzyknięcie ramki YouTube) – wtedy pierwszym stanem otrzymanym jest własnie stan aktywny (ustawiony przez stronę przed odświeżeniem, a ponieważ Python czeka 10 sekund po odłączeniu strony nie zdąży wyłączyć transmisji) i wstrzykujemy ramkę.
Problemy które zauważyłem
Niekiedy ( nie mogę określić dokładnego scenariusza ) ramka YouTube pokazuje nagranie poprzedniej transmisji – wtedy pomaga odświeżenie strony, lub trzeba usunąć nagrane transmisje bezpośrednio w YouTube. W funkcji stopYoutube w Python polecenie pkill jest wywoływane trzykrotnie – niekiedy pierwsze wywołanie nie uśmierca wątku.
Zakończenie
Mam nadzieję ze projekt okaże się przydatny, i dzięki niemu dacie wytchnąć Waszym kamerkom i łączom internetowym 🙂 . Jeżeli macie pomysł na ulepszenie skryptów piszcie na Facebooku projektu. Przypominam, że jest dokumentacja bilbiotek remoteMe gdzie znajdziecie opis funkcji JavaScript, Python i ESP z których korzystam w moich przykładach