[Frage] Softphone oder hook oder was anderes für Türsprechanlage?

demlak

Neuer User
Mitglied seit
25 Mrz 2007
Beiträge
77
Punkte für Reaktionen
1
Punkte
8
Moin,
ich steige grad das erste mal seit rund 20 Jahren wieder in das Thema VOIP ein..

Gerade habe ich ein FreePBX aufgesetzt und mehrere Yealink T42g als Extension und eine Fritzbox als trunk eingerichtet.. läuft alles soweit problemfrei - sowohl intern als auch externes telefonieren geht., voicemail, ivr, etc. auch.

Jetzt mache ich mir Gedanken darüber, wie ich eine noch zu kaufende Türsprechanlage brauchbar umsetzen kann.

Die bisherige Idee: Fanvil i63 verbauen (card-öffner deaktivieren, wegen z.b. flipper-zero, etc.).

Mit gehen dazu zwei unterschiedliche Konzepte durch den Kopf:
1. Wenn auf der Fanvil ein Knopf gedrückt wird, wird eines der Yealink angerufen und die Audio-kommunikation und die Türfreischaltung findet darüber normal statt - parallel wird ein hook ausgeführt, der auf einem Desktop das Kamerabild anzeigt. Gibt es sowas schon in fertig? Oder müsste man sich das erst neu frickeln?

2. Wenn auf der Fanvil ein Knopf gedrückt wird, wird ein Softphone am Desktop gestartet inkl. Kamerabild. Hierbei müssten dann die Desktop-PCs noch Mikrofone bekommen. Die Bedienung findet dann ausschließlich über das Softphone statt. Soweit ich das bisher überblicken kann, würde das mit MicroSIP umsetzbar sein.


Allerdings wollte ich hier mal in die Runde fragen, was es noch für Lösungen gibt. Ich muss mich auch noch damit beschäftigen, ob es sinnvoll wäre einen (hirarchischen) Rundruf an mehrere Extensions dafür einzurichten, etc..

Danke und VG
 
Gibt es sowas schon in fertig?
Nicht, dass ich wüsste.

Soweit ich das bisher überblicken kann, würde das mit MicroSIP umsetzbar sein.
Das wirst du testen müssen. Meiner Erfahrung nach ist Videoübertragung in VoIP Netzen mit unterschiedlichen Herstellern eher ein Glücksspiel.

Dazu hat die Person vor der Tür (z.B. Paketbote) keine Zeit mehr.
 
Wie meinst du das? FreePBX als anderen "Hersteller"?

Gemeint ist mit anderen Herstellern.

- Türsprechstelle von Hersteller A (Fanvil)
- Telefonanlage von Hersteller B (Sangoma=FreePBX)
- Endgeräte von diverseren Herstellern (Yealink, Softphones etc.

Da kommt es schnell zu Unstimmigkeiten in den unterstützen Videocodecs, Auflösungen, Bildwiederholraten usw. die dann dazu führen das es nicht funktioniert.
 
  • Like
Reaktionen: Deleted member 273463
Was man machen kann, ist Video nicht mit dem SIP-Anruf zu machen, sondern "out of band" auf einem Zielgerät einfach den Videostream der jeweiligen Kamera abzugreifen.

Dass es hier garnichts fertiges gibt, würde ich so nicht sagen. Es gibt Automationssysteme die da mehr oder minder Support für haben:

Das würde in Richtung 1. auch gehen. Es gibt durchaus Hersteller/Systeme/wasauchimmer, die diesen "Workflow" so auch unterstützen. "Video-Anrufe über SIP" ist technisch grundlegend möglich und war mal eine tolle Idee, aber es hat sich nie der notwendige Marktdruck für eine Interoperabilität entfaltet, weil das Konzept Video-Anruf von WhatsApp, FaceTime, Zoom,... überholt wurde.
 
moin...

ich hab testhalber mal was mit einem rtsp stream einer anderen Kamera gefrickelt..

- desktop hat einen rudimentären php server laufen.
- php-script führt ein vlc mit dem rtsp stream aus, wenn es richtig aufgerufen wird (get-parameter wie passwort, username, token, caller-ID, etc.)
- das ganze dann im Telefon als action-url bei eingehendem Anruf

Das funktioniert soweit und stellt grundsätzlich die idee ansich erst mal gut dar.

Probleme bleiben aber:
- Bis der Stream angezeigt wird, vergehen fast 4 Sekunden
- Fenstergröße und Fensterposition sind nicht ganz trivial festzulegen (VLC hat dazu angeblich parameter, diese wollen aber nicht(mehr))
- Multiple Instanzen müssten noch verhindert werden
- Abschalten bei Anruf-Ende fehlt
- DND-Status berücksichtigen
- etc..

das nervigste sind die 4 Sekunden bis zum Stream-Aufbau. Aber auch das Gefrickel ansich über einen PHP-Server etc.. ist alles nicht ideal.

edit: mit mpv statt vlc gehts doppelt so schnell..
 
Zuletzt bearbeitet:
Dass es hier garnichts fertiges gibt
Das habe ich auch nicht behauptet, falls ich damit gemeint war. Ich kenne nur nichts, was preislich im Rahmen wäre, nicht direkt eine komplette Telefonanlage mit eigenen Systemkomponenten, oder eine "aufwändige" Automatisierung/Smarthome-Lösung voraussetzt.
Zudem sind hier bereits diverse Komponenten vorgegeben, die die Umsetzung sehr einschränken.
 
Hier mal mein erster Aufschlag, der vielversprechend ist.. von Anruf (klingel betätigen) bis erscheinen des Video-Popups vergeht nur ca. 1 Sekunde.

Mit unseren Yealink T42g funktioniert das wunderbar, wenn bei den Action-URLs bei "Incoming Call" der Startbefehl ausgeführt wird und bei "Terminated" und "Missed Call" der Stopbefehl.

(Bisher nur mit einer normalen IP-Cam und einem Telefon ausprobiert.. die Türklingel ist noch nicht gekauft.)

PHP:
<?php
// PHP-Script zum Anzeigen eines Streams
// Voraussetzung: laufender PHP-Server + mpv
// Das Script ist so angelegt, dass es sowohl unter Windows als auch unter Linux lauffähig ist.

// Beispiellink zum Aufrufen:
// http://192.168.1.10:7888/script.php?username=deinBenutzername&password=deinPasswort&calledNumber=38&calledCommand=CamStart

// ToDo:
// - genauen Prozess killen, statt mpv in allen Instanzen - um zu vermeiden, dass ein parallel-laufender mpv ebenfalls beendet wird.
// - logging inkl. stream-screenshot und/oder Videomitschnitt
// Stand 2026.06.03 - v0.2 demlak

// Benutzername, Passwort und Nummer der Klingel definieren
$valid_username = 'deinBenutzername';   // Benutzername
$valid_password = 'deinPasswort';       // Passwort
$ring_number = '38';                    // Absender-Telefonnummer
$stream_link = 'rtsp://user:[email protected]';        // Link zum Stream inkl. credentials für den Stream. Beispiel: rtsp://user:[email protected]
$max_play_seconds = '60';               // Anzahl Sekunden nachdem der Stream automatisch beendet wird. Dies ist eine Notfalloption

// Befehl zum Beenden des Mediaplayers mpv in Abhängigkeit vom Betriebssystem
if (stristr(PHP_OS, 'WIN')) {
    // Windows-Befehle zum Starten und Beenden
    $command_start_cam = "cmd.exe /c start /b mpv $stream_link --geometry=75%+10+10/2 --profile=low-latency --end=$max_play_seconds";
    $command_stop_cam  = 'taskkill /F /IM mpv.exe';
} elseif (stristr(PHP_OS, 'LINUX')) {
    // Linux-Befehl zum Starten und Beenden
    $command_start_cam = "mpv $stream_link --geometry=75%+10+10/2 --profile=low-latency --end=$max_play_seconds > /dev/null 2>&1 &";
    $command_stop_cam  = 'killall mpv > /dev/null 2>&1 &';
} else {
    echo "Unbekanntes Betriebssystem - breche php-script ab";
    exit();
}


// Überprüfen, ob die Parameter gesetzt sind
if (isset($_GET['username']) && isset($_GET['password']) && isset($_GET['calledNumber']) && isset($_GET['calledCommand'])) {

    // Parameter einsammeln
    $username = htmlspecialchars($_GET['username']);
    $password = htmlspecialchars($_GET['password']);
    $calledNumber = htmlspecialchars($_GET['calledNumber']);
    $calledCommand = htmlspecialchars($_GET['calledCommand']);

    // Benutzername, Passwort und Absender-Telefonnummer überprüfen
    if ($username === $valid_username && $password === $valid_password && $calledNumber === $ring_number) {
        if ($calledCommand === 'CamStart') {
            pclose(popen($command_start_cam, 'r'));
            echo "Befehl $calledCommand wurde erfolgreich ausgeführt.";
        } elseif ($calledCommand === 'CamStop') {
            pclose(popen($command_stop_cam, 'r'));
            echo "Befehl $calledCommand wurde erfolgreich ausgeführt.";
        } else {
        echo "Ungültiger Befehl";
        }

    } else {
        echo "Kombination aus Benutzername, Passwort und Absender-Telefonnummer falsch";
    }
} else {
    echo "Bitte Benutzername, Passwort, Absender-Telefonnummer und Kommando in der URL angeben.";
}

?>
 
Zuletzt bearbeitet:
@tomeko danke

Ich bin noch etwas tiefer eingestiegen und die fanvil i63 ist nun auch da.. ich denke, es wird kein softphone und auch generell wohl eher keine fertige/fremde software.. ich denke, ich bleibe dabei selber was zu frickeln, um den vorherrschenden Bedürfnissen flexibel gerecht zu werden.

ich hab den obigen php-code inzwischen in golang umgesetzt und auch etwas erweitert.. ich frickel da noch weiter rum und wenn jemand interesse hat, einfach melden..
 
Zuletzt bearbeitet:
ich habe das Ganze mit einer Doorbird D2100E und mehreren Yealink T58W gelöst, früher über die Fritzbox eine Rufgruppe für die Klingel erstellt, damit haben alle gewünschten Telefone geklingelt und das Bild der Kamera angezeigt, zwischenzeitlich bin ich von der Fritzbox weg als "Telefonanlage" und habe eine Yeastar P520 im Einsatz, auch hier funktioniert der Gruppenruf mit Kamerabild ausgezeichnet. Die Türe kann direkt von den Yealinks (auch ältere Telefone, die das Kamerabild nicht anzeigen können) bei Ruf oder auch als DSS-Key über eine URL geöffnet werden. Die Sprechanlage kann auch ohne klingeln per "programmierbarer Taste" aufgerufen werden, bei den T58W selbstverständlich auch mit Kamerabild.

Eine Anzeige des Kamerabilds am Bildschirm des PC wäre aber noch eine tolle zusätzliche Funktion, an der ich interessiert wäre.
 
dann hier mal der Zwischenstand.. kein Plan, ob dir das weiterhilft..

Code:
// Crossbuild for Windows: GOFLAGS=-buildvcs=false GOOS=windows GOARCH=386 go build -ldflags -H=windowsgui Main.go
// crossbuild for win: GOFLAGS=-buildvcs=false GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -tags gl -ldflags -H=windowsgui Main.go

// Todo:
// - mpv parameter "--ontop" optional über config
// - checken ob mpv erreichbar is
// - logging des streams in unterordner - nur die letzten 10 (config.txt)
// - Mailbenachrichtigung inkl. screenshot
// - code aufsplitten in
//  - ?Main
//  - ?Server
//  - ?mpv
//  - ?helpfile
//  - ?logging

// - mehrere streams gleichzeitig? mpv rtsp://foo:[email protected] --external-file=rtsp://foo:[email protected] --lavfi-complex='[vid1] scale=960x540 [vid1_scale]; [vid2] scale=960x540 [vid2_scale]; [vid1_scale][vid2_scale] vstack [vo]' --geometry=75%+10+10/2 --profile=low-latency --ontop

// Stand 2026.06.15 - v0.4 demlak

package main

import (
    _ "embed"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
    "strings"
    "sync"
    "time"

    "github.com/getlantern/systray"
)

//go:embed assets/icons/icon.png
var iconpng []byte

//go:embed assets/icons/icon.ico
var iconico []byte

type Config map[string]string

var (
    mpvCmd  *exec.Cmd
    mpvMu   sync.Mutex
    logFile *os.File
)

func initLogFile(dir string) error {
    var err error
    logPath := filepath.Join(dir, "mpv-controller.log")
    logFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return fmt.Errorf("Logdatei konnte nicht geöffnet werden: %v", err)
    }
    return nil
}

func logMsg(msg string) {
    timestamp := time.Now().Format("2006-01-02 15:04:05")
    line := fmt.Sprintf("[%s] %s", timestamp, msg)
    log.Println(msg)
    if logFile != nil {
        fmt.Fprintln(logFile, line)
    }
}

func loadConfig(path string) (Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }
    cfg := Config{}
    for _, line := range strings.Split(string(data), "\n") {
        line = strings.TrimSpace(line)
        if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
            continue
        }
        if idx := strings.Index(line, "="); idx >= 0 {
            k := strings.TrimSpace(line[:idx])
            v := strings.TrimSpace(line[idx+1:])
            v = strings.Trim(v, `"'`)
            cfg[k] = v
        }
    }
    return cfg, nil
}

func mpvStart(cfg Config, source string) error {
    mpvMu.Lock()
    defer mpvMu.Unlock()

    if mpvCmd != nil && mpvCmd.Process != nil {
        pid := mpvCmd.Process.Pid
        if err := mpvCmd.Process.Kill(); err != nil {
            return fmt.Errorf("Fehler beim Beenden der laufenden mpv-Instanz (PID %d): %v", pid, err)
        }
        logMsg(fmt.Sprintf("laufende mpv-Instanz präventiv beendet (PID %d)", pid))
        mpvCmd = nil
    }

    link := cfg["STREAM_LINK"]
    maxSec := cfg["MAX_PLAY_SECONDS"]
    if link == "" {
        return fmt.Errorf("STREAM_LINK fehlt in config")
    }
    args := []string{link, "--geometry=75%+10+10/2", "--profile=low-latency", "--ontop"}
    if maxSec != "" {
        args = append(args, "--end="+maxSec)
    }

    wd, err := os.Getwd()
    if err != nil {
        return fmt.Errorf("Fehler beim Ermitteln des aktuellen Verzeichnisses: %v", err)
    }

    if runtime.GOOS == "windows" {
        mpvCmd = exec.Command(filepath.Join(wd, "mpv.exe"), args...)
    } else {
        mpvCmd = exec.Command("mpv", args...)
    }

    if err := mpvCmd.Start(); err != nil {
        mpvCmd = nil
        return fmt.Errorf("Fehler beim Starten von mpv: %v", err)
    }

    logMsg(fmt.Sprintf("mpv gestartet (PID %d) [Quelle: %s]", mpvCmd.Process.Pid, source))
    return nil
}

func mpvStop(source string) error {
    mpvMu.Lock()
    defer mpvMu.Unlock()

    if mpvCmd == nil || mpvCmd.Process == nil {
        return fmt.Errorf("kein laufender mpv-Prozess gefunden")
    }

    pid := mpvCmd.Process.Pid
    if err := mpvCmd.Process.Kill(); err != nil {
        return fmt.Errorf("Fehler beim Beenden von mpv (PID %d): %v", pid, err)
    }

    logMsg(fmt.Sprintf("mpv gestoppt (PID %d) [Quelle: %s]", pid, source))
    mpvCmd = nil
    return nil
}

func senderIP(r *http.Request) string {
    if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
        return forwarded
    }
    return r.RemoteAddr
}

func main() {
    exe, err := os.Executable()
    if err != nil {
        log.Fatal("kann Pfad zur ausführbaren Datei nicht ermitteln:", err)
    }
    dir := filepath.Dir(exe)

    if err := initLogFile(dir); err != nil {
        log.Println("Warnung:", err)
    }
    defer logFile.Close()
    logMsg("Programm gestartet")

    cfgPath := filepath.Join(dir, "config.txt")
    cfg, err := loadConfig(cfgPath)
    if err != nil {
        log.Fatalf("Konfigurationsdatei config.txt nicht gefunden oder nicht lesbar: %v", err)
    }

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        username := r.URL.Query().Get("username")
        password := r.URL.Query().Get("password")
        calledNumber := r.URL.Query().Get("calledNumber")
        calledCommand := r.URL.Query().Get("calledCommand")

        if username == "" || password == "" || calledNumber == "" || calledCommand == "" {
            http.Error(w, "Bitte Benutzername, Passwort, Absender-Telefonnummer und Kommando in der URL angeben.", http.StatusBadRequest)
            return
        }

        if username != cfg["VALID_USERNAME"] || password != cfg["VALID_PASSWORD"] || calledNumber != cfg["RING_NUMBER"] {
            http.Error(w, "Kombination aus Benutzername, Passwort und Absender-Telefonnummer falsch", http.StatusUnauthorized)
            return
        }

        ip := senderIP(r)
        source := fmt.Sprintf("Webhook [IP: %s]", ip)

        switch calledCommand {
        case "CamStart":
            logMsg(fmt.Sprintf("CamStart empfangen [IP: %s]", ip))
            if err := mpvStart(cfg, source); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            fmt.Fprintln(w, "Befehl CamStart wurde erfolgreich ausgeführt.")
        case "CamStop":
            logMsg(fmt.Sprintf("CamStop empfangen [IP: %s]", ip))
            if err := mpvStop(source); err != nil {
                fmt.Fprintln(w, "Fehler beim Stoppen von mpv:", err)
                return
            }
            fmt.Fprintln(w, "Befehl CamStop wurde erfolgreich ausgeführt.")
        default:
            http.Error(w, "Ungültiger Befehl", http.StatusBadRequest)
        }
    })

    go func() {
        port := cfg["PORT"]
        if port == "" {
            port = "8080"
        }
        logMsg(fmt.Sprintf("HTTP-Server läuft auf :%s", port))
        if err := http.ListenAndServe(":"+port, nil); err != nil {
            log.Fatal("HTTP-Server Fehler:", err)
        }
    }()

    onReady := func() {
        if runtime.GOOS == "windows" {
            systray.SetIcon(iconico)
        } else {
            systray.SetIcon(iconpng)
        }
        systray.SetTitle("mpv-controller")
        systray.SetTooltip("mpv remote controller (HTTP)")

        mStart := systray.AddMenuItem("Start mpv", "Startet mpv manuell")
        mStop := systray.AddMenuItem("Stop mpv", "Beendet mpv manuell")
        systray.AddSeparator()
        mQuit := systray.AddMenuItem("Beenden", "Beendet das Programm")

        go func() {
            for {
                select {
                case <-mStart.ClickedCh:
                    if err := mpvStart(cfg, "Systray"); err != nil {
                        logMsg("Start-Fehler: " + err.Error())
                    }
                case <-mStop.ClickedCh:
                    if err := mpvStop("Systray"); err != nil {
                        logMsg("Stop-Fehler: " + err.Error())
                    }
                case <-mQuit.ClickedCh:
                    systray.Quit()
                    return
                }
            }
        }()
    }

    onExit := func() {
        logMsg("Programm beendet")
    }

    systray.Run(onReady, onExit)
}
 
Inzwischen überlege ich, ob es nicht doch sinnvoller wäre mit einem zentralen Server zu arbeiten, statt auf jedem telefon und jedem desktop eine eigene komplette konfiguration zu haben.

Man müsste auf Desktop (Client) und am Telefon (Action URL zum Server) jeweils nur Benutzername und Passwort individualisieren.
Der Rest wäre dann zentral für alle Desktops und Telefone an einem Punkt zu managen.

Wenn man dann noch eine Routine einbaut, mit der die Client-App auf dem Desktop-Rechner sich selber ein update vom Server holt, gäbe es auch die Option später weitere Features einzubauen, ohne an den Clients rumzufummeln.. einfach am Server hinterlegen.

Ich überlege nämlich auch noch ein Popup für Telefonbucheinträge am Desktop umzusetzen.. hat aber jetzt nix mit dem Ausgangsthema hier im Thread zu tun..
 
  • Like
Reaktionen: Jogi29
Kostenlos!

Statistik des Forums

Themen
248,847
Beiträge
2,302,780
Mitglieder
378,497
Neuestes Mitglied
guenther1987