Using a raspberry pi as a media streamer remains one of the most reliable ways to bypass the sluggish, ad-heavy interfaces baked into modern smart TVs. While commercial streaming boxes are convenient, they lock you out of local network playback, custom add-ons, and lossless audio routing. By building your own embedded streamer, you gain total control over the hardware pipeline.
This guide cuts through the noise to give you a definitive hardware decision, a precise wiring schematic for a physical GPIO control panel, and the complete Python code to bridge those physical buttons to Kodi's JSON-RPC API. We are targeting the Raspberry Pi 5 (4GB) running LibreELEC 12 (Omega), as it is the current sweet spot for hardware AV1 decoding and dual-4K output.
The Verdict: Choosing Your Pi Board
Before buying parts, you need to match the silicon to your display and network requirements. The Pi 5 introduced a dedicated RP1 I/O controller and a hardware AV1 decoder, which fundamentally changed its viability for modern streaming compared to the Pi 4.
| If your requirement is... | Choose this board | Why it wins |
|---|---|---|
| Dual 4K @ 60Hz, AV1 hardware decode, local AI upscaling | Raspberry Pi 5 (4GB) | AV1 decode prevents CPU bottlenecking on modern YouTube/Netflix streams; RP1 chip handles dual micro-HDMI smoothly. |
| Single 4K HDR, lowest possible power draw, passive cooling | Raspberry Pi 4 Model B (4GB) | Mature ecosystem, runs cool enough for a passive aluminum case, handles HEVC/H.265 perfectly. |
| Portable 1080p streaming, battery-powered, ultra-compact | Raspberry Pi Zero 2 W | Draws under 1.5W, fits behind any monitor, but lacks hardware 4K decode and full-size HDMI. |
Parts List & GPIO Pin Mapping
This build integrates a physical 3-button control panel (Play/Pause, Volume Up, Volume Down) directly into the Pi's GPIO header. This is highly useful for headless setups, kiosk modes, or when the TV remote's CEC handshake inevitably fails.
Bill of Materials (BOM)
- Compute: Raspberry Pi 5 (4GB) - ~$60
- Thermal: Official Raspberry Pi Active Cooler - ~$5 (Do not use passive cases on the Pi 5; the BCM2712 throttles at 80°C under video decode loads).
- Power: Official 27W USB-C PD Power Supply - ~$12 (Critical: Third-party 5V/3A supplies will trigger peripheral brownout warnings on the Pi 5).
- Storage: SanDisk Extreme 64GB A2 U3 microSD - ~$15
- Inputs: 3x 12mm Momentary Tactile Push Button Switches
- Passives: 3x 10kΩ Resistors (for external pull-up stability, though internal pull-ups are enabled in software)
- Wiring: Female-to-Male Dupont jumper wires
Pin Mapping Table
We are wiring the switches between the 3.3V rail and the GPIO pins. The script will configure the GPIO pins with internal pull-downs, registering a HIGH state when pressed.
| Component | Switch Pin 1 | Switch Pin 2 | Pi 5 GPIO / Power Pin | BCM GPIO Number |
|---|---|---|---|---|
| Play / Pause Button | 3.3V (Pin 1) | Signal | Pin 11 | GPIO 17 |
| Volume Up Button | 3.3V (Pin 1) | Signal | Pin 13 | GPIO 27 |
| Volume Down Button | 3.3V (Pin 17) | Signal | Pin 15 | GPIO 22 |
Wiring the Physical Control Panel
- Seat the Active Cooler: Align the thermal pads with the BCM2712 CPU and RP1 chip. Press down firmly until the push-pins click into the mounting holes. Connect the 4-pin PWM fan cable to the dedicated JST fan header on the Pi 5 (located near the USB ports), not the 5V GPIO pins.
- Wire the Power Rail: Connect a jumper wire from Pin 1 (3.3V) to the positive rail on your breadboard.
- Wire the Switches: Insert the three tactile switches across the breadboard center trench. Connect one leg of each switch to the 3.3V positive rail.
- Wire the GPIO Signals: Connect the opposite leg of the Play/Pause switch to Pin 11 (GPIO 17), Volume Up to Pin 13 (GPIO 27), and Volume Down to Pin 15 (GPIO 22).
- Flash LibreELEC: Use the official Raspberry Pi Imager to flash LibreELEC 12 (Omega) for the Pi 5 onto your microSD card. In the Imager settings, enable SSH and set a static IP if preferred, though Kodi's web server will use localhost for this script.
The Code: GPIO to Kodi JSON-RPC Bridge
To control Kodi without relying on flaky HDMI-CEC commands, we use Kodi's built-in JSON-RPC API. The following Python script uses the gpiozero library to detect button presses and the requests library to send HTTP POST payloads to Kodi.
Target Board: Raspberry Pi 5 (4GB). Requires LibreELEC or Raspberry Pi OS with sudo apt install python3-gpiozero python3-lgpio python3-requests.
#!/usr/bin/env python3
import requests
import json
import logging
from gpiozero import Button
from signal import pause
# --- Configuration & Pin Definitions ---
PIN_PLAY_PAUSE = 17
PIN_VOL_UP = 27
PIN_VOL_DOWN = 22
KODI_IP = '127.0.0.1'
KODI_PORT = 8080
KODI_USER = 'kodi'
KODI_PASS = '' # Leave blank if no password is set in Kodi
KODI_URL = f'http://{KODI_IP}:{KODI_PORT}/jsonrpc'
# Setup logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# Initialize Buttons with bounce_time to prevent contact chatter
btn_play = Button(PIN_PLAY_PAUSE, bounce_time=0.05, pull_up=False)
btn_vol_up = Button(PIN_VOL_UP, bounce_time=0.05, pull_up=False)
btn_vol_down = Button(PIN_VOL_DOWN, bounce_time=0.05, pull_up=False)
def send_kodi_rpc(method, params=None):
"""Sends a JSON-RPC payload to Kodi and handles connection errors."""
headers = {'Content-Type': 'application/json'}
payload = {
'jsonrpc': '2.0',
'method': method,
'id': 1
}
if params:
payload['params'] = params
try:
response = requests.post(
KODI_URL,
data=json.dumps(payload),
headers=headers,
auth=(KODI_USER, KODI_PASS),
timeout=2
)
response.raise_for_status()
logging.info(f'Success: {method}')
except requests.exceptions.ConnectionError as e:
logging.error(f'Connection failed. Is Kodi running and JSON-RPC enabled? Error: {e}')
except requests.exceptions.RequestException as e:
logging.error(f'RPC Request failed: {e}')
# --- Button Callbacks ---
def on_play_pause():
logging.info('Play/Pause pressed')
send_kodi_rpc('Player.PlayPause', {'playerid': 0}) # 0 is usually the active video player
# Fallback to playerid 1 if audio is playing
send_kodi_rpc('Player.PlayPause', {'playerid': 1})
def on_vol_up():
logging.info('Volume Up pressed')
# Increment volume by 5%
send_kodi_rpc('Application.SetVolume', {'volume': 'increment'})
def on_vol_down():
logging.info('Volume Down pressed')
send_kodi_rpc('Application.SetVolume', {'volume': 'decrement'})
# --- Bind Events ---
btn_play.when_pressed = on_play_pause
btn_vol_up.when_pressed = on_vol_up
btn_vol_down.when_pressed = on_vol_down
if __name__ == '__main__':
logging.info('Kodi GPIO Bridge started. Waiting for button presses...')
# Keep the script running
pause()
Deployment Note: Save this as kodi_gpio_bridge.py. To run it on boot in LibreELEC, place it in /storage/.config/autostart.sh or create a systemd service in Raspberry Pi OS. Ensure you enable the web server in Kodi first.
Debugging: When the API Refuses to Connect
The most common failure point in this build is not the hardware wiring, but the network handshake between the Python script and Kodi. If your buttons do nothing and your logs spit out the following error, follow the ranked troubleshooting path below.
Exact Error String:
requests.exceptions.ConnectionError: HTTPConnectionPool(host='127.0.0.1', port=8080): Max retries exceeded with url: /jsonrpc (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object at 0x7f8b9c0>: Failed to establish a new connection: [Errno 111] Connection refused'))
The First Three Things to Check
- Kodi Web Server is Disabled (90% of cases): By default, Kodi's JSON-RPC API is turned off for security. Navigate in Kodi to Settings (Gear Icon) > Services > Control. Toggle Allow remote control via HTTP to ON. Ensure the port is set to 8080.
- Missing
lgpioBackend on Pi 5: If the script crashes immediately with aPinFactoryFallbackorModuleNotFoundError: No module named 'lgpio', it meansgpiozerocannot talk to the Pi 5's RP1 chip. The legacyRPi.GPIOlibrary is deprecated on Pi 5. Fix this by runningsudo apt install python3-lgpioin the terminal. - Local Firewall Blocking Loopback: If you are running a hardened Raspberry Pi OS build with
ufwenabled, port 8080 might be blocked even on localhost. Runsudo ufw allow 8080/tcpor temporarily disable the firewall to isolate the issue.
Extending or Simplifying the Build
Once the baseline GPIO bridge is stable, you can adapt the hardware to fit your specific living room constraints.
How to Extend (Audiophile Output): The Pi 5's native 3.5mm audio jack is PWM-based and suffers from a high noise floor, making it unsuitable for high-end stereo receivers. To fix this, stack an I2S DAC HAT like the HiFiBerry DAC+ Standard onto the GPIO header. This bypasses the Pi's internal audio routing entirely, feeding lossless digital audio directly to a Texas Instruments PCM5102a DAC chip. You will need to add dtoverlay=hifiberry-dacplus to your config.txt file to enable it, but the resulting signal-to-noise ratio is indistinguishable from dedicated $500 streamers.
Building your own streamer eliminates the planned obsolescence of commercial dongles. By anchoring your build on the Pi 5's AV1 decode capabilities and bridging physical controls directly to the Kodi API, you create a media endpoint that is both future-proof and entirely under your command.






