Skip to content
xfangfang edited this page Jan 25, 2022 · 1 revision

With just a few lines of code, you can publish your device as a DLNA renderer.

Tutorial

1. install macast

First, You need install macast via pip. see: https://github.com/xfangfang/Macast/wiki/Installation#pip

2. run the minimal demo

Then refer to the following example or MPVRenderer (located at macast_renderer/mpv.py) create your own CustomRenderer class which inherits the Renderer class.

# a minimal demo
# this demo will print the media url when you push a media file through your phone

from macast import cli, Renderer

class CustomRenderer(Renderer):
    def set_media_url(self, url):
        print(url)

if __name__ == '__main__':
    cli(CustomRenderer())

3. set Macast metadata

Please imitate the following headers to write your headers, otherwise Macast will not recognize your Python file as a plugin.

# a minimal demo
# this demo will print the media url when you push a media file through your phone

# Macast Metadata
# <macast.title>DEMO Renderer</macast.title>
# <macast.renderer>CustomRenderer</macast.renderer>
# <macast.platform>darwin,win32,linux</macast.platform>
# <macast.version>0.1</macast.version>
# <macast.author>xfangfang</macast.author>
# <macast.desc>this demo will print the media url when you push a media file through your phone</macast.desc>

from macast import cli, Renderer

class CustomRenderer(Renderer):
    def set_media_url(self, url):
        print(url)

if __name__ == '__main__':
    cli(CustomRenderer())

4. more api

  • You need to implement part of the following method to get data from DLNA clients.(At least set_media_url needs to be implemented to obtain the media url from DLNA clients.)

    Methods needed to be implemented:
    def set_media_stop(self):
        pass
    
    def set_media_pause(self):
        pass
    
    def set_media_resume(self):
        pass
    
    def set_media_volume(self, data):
        """ data : int, range from 0 to 100
        """
        pass
    
    def set_media_mute(self, data):
        """ data : bool
        """
        pass
    
    def set_media_url(self, data):
        """ data : string
        """
        pass
    
    def set_media_title(self, data):
        """ data : string
        """
        pass
    
    def set_media_position(self, data):
        """ data : string position, 00:00:00
        """
        pass
  • When the player(CustomRenderer) running on the computer is operated by the user, you need to call some of the following methods to notify the DLNA clients of the new changes.

    Methods needed to be called:
    # The current position of the media you are playing
    # ATTENTION: Not to call this method in the self.set_media_position callback
    # data : string, 00:00:00
    self.set_state_position(data)
    
    # The length of the media you are playing
    # data : string, 00:00:00
    self.set_state_duration(data):
    
    # Very important!!!
    # Call this function when the state of the played video changes
    # PLAYING and STOPPED must be implemented
    # data : string in [PLAYING, PAUSED_PLAYBACK, STOPPED]
    self.set_state_transport(data):
    
    # If there is an error in the media you play (for example, the file format is not supported), call this method
    self.set_state_transport_error():
    
    # ATTENTION: Not to call this method in the self.set_media_mute callback
    # data : bool
    self.set_state_mute(data):
    
    # ATTENTION: Not to call this method in the self.set_media_volume callback
    # data : int, range from 0 to 100
    self.set_state_volume(data)

Examples

The following examples do not contain Macast metadata. Please go to Macast-plugins to find the complete code.

1. example IINA

Many people asked me if Macast could support IINA. Now a few lines of code which add supporting of iina for Macast is here.
This example only supports the control of IINA‘s start and stop. You can complete more functions base the code below.
In the future, macast will support the selection of players. Welcome to contribute your code then.

IINA Renderer code:
# Copyright (c) 2021 by xfangfang. All Rights Reserved.
#
# Using iina as DLNA media renderer
#

import os
import time
import threading
import subprocess
from macast import cli
from macast.renderer import Renderer

IINA_PATH = '/Applications/IINA.app/Contents/MacOS/iina-cli'


class IINARenderer(Renderer):
    def __init__(self):
        super(IINARenderer, self).__init__()
        self.start_position = 0
        self.position_thread_running = True
        self.position_thread = threading.Thread(target=self.position_tick, daemon=True)
        self.position_thread.start()
        # a thread is started here to increase the playback position once per second
        # to simulate that the media is playing.
        self.iina = None

    def position_tick(self):
        while self.position_thread_running:
            time.sleep(1)
            self.start_position += 1
            sec = self.start_position
            position = '%d:%02d:%02d' % (sec // 3600, (sec % 3600) // 60, sec % 60)
            self.set_state_position(position)

    def set_media_stop(self):
        if self.iina is not None:
            self.iina.terminate()
        try:
            os.waitpid(-1, 1)
        except Exception as e:
            pass
        self.set_state_transport('STOPPED')

    def set_media_url(self, url):
        self.start_position = 0
        self.iina = subprocess.Popen([IINA_PATH, '--keep-running', url])
        self.set_state_transport("PLAYING")

    def stop(self):
        super(IINARenderer, self).stop()
        self.set_media_stop()


if __name__ == '__main__':
    cli(IINARenderer())

2. example PotPlayer

The code is almost the same as that of IINA, only slightly different when the player is turned on and off.

PotPlayer Renderer code:
# Copyright (c) 2021 by xfangfang. All Rights Reserved.
#
# Using potplayer as DLNA media renderer
#

import os
import time
import threading
import subprocess
from macast import cli, gui
from macast.renderer import Renderer

# POTPLAYER_PATH = '%HOMEPATH%\\Downloads\\PotPlayer64\\PotPlayermini64.exe'
POTPLAYER_PATH = '"C:\\Program Files\\PotPlayer64\\PotPlayermini64.exe"'


class PotplayerRenderer(Renderer):
    def __init__(self):
        super(PotplayerRenderer, self).__init__()
        self.start_position = 0
        self.position_thread_running = True
        self.position_thread = threading.Thread(target=self.position_tick, daemon=True)
        self.position_thread.start()
        # a thread is started here to increase the playback position once per second
        # to simulate that the media is playing.

    def position_tick(self):
        while self.position_thread_running:
            time.sleep(1)
            self.start_position += 1
            sec = self.start_position
            position = '%d:%02d:%02d' % (sec // 3600, (sec % 3600) // 60, sec % 60)
            self.set_state_position(position)

    def set_media_stop(self):
        subprocess.Popen(['taskkill', '/f', '/im', 'PotPlayerMini64.exe']).communicate()
        self.set_state_transport('STOPPED')

    def set_media_url(self, url):
        self.set_media_stop()
        self.start_position = 0
        subprocess.Popen('{} "{}"'.format(POTPLAYER_PATH, url), shell=True)
        self.set_state_transport("PLAYING")

    def stop(self):
        super(PotplayerRenderer, self).stop()
        self.set_media_stop()
        print("PotPlayer stop")

    def start(self):
        super(PotplayerRenderer, self).start()
        print("PotPlayer start")


if __name__ == '__main__':
    gui(PotplayerRenderer())
    # or using cli to disable taskbar menu
    # cli(PotplayerRenderer())

3. example PI-FM-RDS

This example runs on raspberry pi,it receives MP3 audio from DLNA client, and sends FM broadcast through PiFmRds

Warning and Disclaimer
PiFmRds is an experimental program, designed only for experimentation. It is in no way intended to become a personal media center or a tool to operate a radio station, or even broadcast sound to one's own stereo system.

In most countries, transmitting radio waves without a state-issued licence specific to the transmission modalities (frequency, power, bandwidth, etc.) is illegal.

Therefore, always connect a shielded transmission line from the RaspberryPi directly to a radio receiver, so as not to emit radio waves. Never use an antenna.

Even if you are a licensed amateur radio operator, using PiFmRds to transmit radio waves on ham frequencies without any filtering between the RaspberryPi and an antenna is most probably illegal because the square-wave carrier is very rich in harmonics, so the bandwidth requirements are likely not met.

I could not be held liable for any misuse of your own Raspberry Pi. Any experiment is made under your own responsibility.

PI FM RDS Renderer code:
# Copyright (c) 2021 by xfangfang. All Rights Reserved.
#
# Using pi_fm_rds as DLNA media renderer
# https://github.com/ChristopheJacquet/PiFmRds
#

import os
import time
import threading
import subprocess
from macast import cli, gui
from macast.renderer import Renderer


class PIFMRenderer(Renderer):
    def __init__(self):
        super(PIFMRenderer, self).__init__()
        self.start_position = 0
        self.position_thread_running = True
        self.position_thread = threading.Thread(target=self.position_tick, daemon=True)
        self.position_thread.start()
        # Because the playback progress cannot be easily obtained from **sox**,
        # a thread is started here to increase the playback position once per second
        # to simulate that the audio is playing.
        self.sox = None
        self.fm = subprocess.Popen(['sudo', 'pi_fm_rds', '-freq', '108', '-audio', '-'],
                                   stdin=subprocess.PIPE,
                                   bufsize=1024)

    def position_tick(self):
        while self.position_thread_running:
            time.sleep(1)
            self.start_position += 1
            sec = self.start_position
            position = '%d:%02d:%02d' % (sec // 3600, (sec % 3600) // 60, sec % 60)
            self.set_state_position(position)

    def set_media_stop(self):
        if self.sox is not None:
            self.sox.terminate()
        os.waitpid(-1, 1)
        self.set_state_transport('STOPPED')

    def set_media_url(self, data):
        self.start_position = 0
        self.sox = subprocess.Popen(['sox', '-t', 'mp3', data, '-t', 'wav', '-'],
                                    stdout=self.fm.stdin)
        self.set_state_transport("PLAYING")

    def stop(self):
        super(PIFMRenderer, self).stop()
        os._exit(0)


if __name__ == '__main__':
    cli(PIFMRenderer())
    # or using gui
    # gui(PIFMRenderer())