Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverepper committed Mar 13, 2023
0 parents commit f6a212a
Show file tree
Hide file tree
Showing 19 changed files with 1,150 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
build
cmake-build-debug
.idea
TODO.md
30 changes: 30 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.3)

project(phone VERSION 0.1 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 20)

set(CMAKE_C_VISIBILITY_PRESET hidden)
set(CMAKE_CXX_VISIBILITY_PRESET hidden)
set(CMAKE_VISIBILITY_INLINES_HIDDEN true)

include(GNUInstallDirs)

file(RELATIVE_PATH pathToLibs
${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}
${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR})

list(APPEND CMAKE_MESSAGE_INDENT "🔧 ")
if(APPLE)
message(DEBUG "Configuring for Apple platform")
set(CMAKE_INSTALL_RPATH "@loader_path/${pathToLibs}")
elseif(UNIX)
message(DEBUG "Configuring for UNIX")
set(CMAKE_INSTALL_RPATH "$ORIGIN/${pathToLibs}")
endif()
message(DEBUG "CMAKE_INSTALL_RPATH: ${CMAKE_INSTALL_RPATH}")
list(POP_BACK CMAKE_MESSAGE_INDENT)

add_subdirectory(src bin)

install(TARGETS phone_sharedlib)
179 changes: 179 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# libphone

libphone is a library that sits on top of [PJSIP project](https://github.com/pjsip/pjproject) and tries to make it very simple to build a softphone. libphone provides a C++ and a C-API. Included with the library is a Python script that demonstrates the API [python_cli.py](src/python_cli/python_cli.py). You can download one of the [binaries](#binaries) and just enter your SIP credentials if you want to test things out.

## Overview
<!-- TOC -->
* [libphone](#libphone)
* [Overview](#overview)
* [Usage](#usage)
* [Callbacks](#callbacks)
* [CLI Demo Phone](#cli-demo-phone)
* [Compatibility with SIP providers](#compatibility-with-sip-providers)
* [Binaries](#binaries)
* [Build instructions for Linux](#build-instructions-for-linux)
* [Build instructions for macOS](#build-instructions-for-macos)
<!-- TOC -->

## Usage

You can create a phone with

```python
phone = phone_create("☎️", ["194.25.0.69","194.25.0.79"], "stun.t-online.de")
```

and connect the phone to your server via:

```python
phone_connect(phone, "tel.t-online.de", "your_sip_username", "your_sip_password")
```
Now you are ready to make a call:

```python
phone_make_call(phone, "+491804100100")
```

### Callbacks
You can register callback functions for incoming calls and their call states like this:

```python
phone_register_on_incoming_call_callback(phone, on_incoming_call, None)
phone_register_on_call_state_callback(phone, on_call_state, None)
```

The third parameter to the callback functions is required if you want to use the C-API with an object-oriented programming language like Swift:

```swift
phone_register_on_incoming_call_callback(phone, { callId, ctx in
guard let ctx else { return }
let me = Unmanaged<Model>.fromOpaque(ctx).takeUnretainedValue()
DispatchQueue.main.async {
me.current_call_id = callId
}
}, Unmanaged.passRetained(self).toOpaque())
```

## CLI Demo Phone

![CLI Demo Phone](python_cli.png)

Once you have the library installed there is a CLI Demo phone written in Python in the bin directory `libphone/bin/python_cli.py`. If you want to make a phone call you need to update the config section in the beginning of the script:

```python
useragent = "Python CLI Phone"
nameservers = ["217.237.148.22", "217.237.150.51"]
stunservers = ["stun.t-online.de"]
sipserver = "tel.t-online.de"
username = "+49..."
password = None
opus_channel_count = 1
opus_complexity = 8
opus_sample_rate = 16000
buddy = "+491804100100"
```

If you are a customer of DTAG (Deutsche Telekom AG) you can simply put in your username (which is your telephone number) and everything is set up. A password is not required for DTAG since your line-id is used for authentication.

## Compatibility with SIP providers

libphone should work with almost every SIP-provider out of the box. It's been tested with:
- DTAG (Deutsche Telekom)
- Starface
- TeamFON

I'd be very happy if you can test with your provider and give me feedback. I am willing to help. Reach me here [Oliver Epper](https://oliver-epper.de).

## Binaries

If you want to try this out I maintain the following binary distributions:

- [libphone.0.0.1-macos-universal.zip](https://oliver-epper.de/libphone.0.0.1-macos-universal.zip) (signed and notarized, ready to go)
- [libphone.0.0.1-ubuntu22.04-aarch64.tgz](https://oliver-epper.de/libphone.0.0.1-ubuntu22.04-aarch64.tgz)
- [libphone.0.0.1-ubuntu22.04-x86_64.tgz](https://oliver-epper.de/libphone.0.0.1-ubuntu22.04-x86_64.tgz)

## Build instructions for Linux

I've tested this on Ubuntu 22.04.

First install the build-dependencies:

```shell
apt install build-essential git pkg-config cmake python3-minimal libssl-dev libasound2-dev libopus-dev ninja-build
```

Ninja ist not required, but much faster than building with make.
Check out pjproject:

```shell
git -c advice.detachedHead=false clone --depth 1 --branch "2.13" https://github.com/pjsip/pjproject
```

This will give you the 2.13 release of pjproject which I tested.
Build pjproject with the following commands:

```shell
pushd pjproject
cat << EOF > pjlib/include/pj/config_site.h
#define PJ_HAS_SSL_SOCK 1
#include <pj/config_site_sample.h>
EOF
cat << EOF > user.mak
export CFLAGS += -fPIC
EOF
./configure --prefix=$(echo ~)/installed/pjproject
make dep && make clean
make
make install
popd
```

Once installed into `~/installed/pjproject` you need to change two things:

```shell
pushd ~/installed/pjproject
ar -rcT lib/libpjproject.a lib/*.a
cp lib/pkgconfig/libpjproject.pc lib/pkgconfig/libpjproject.orig
popd
```

Please edit the file `~/installed/pjproject/lib/pkgconfig/libpjproject.pc` to contain the following (example from my build-docker):

```text
prefix=/home/oliver/installed/pjproject
libdir=${prefix}/lib
includedir=${prefix}/include
Name: libpjproject
Description: Multimedia communication library
URL: http://www.pjsip.org
Version: 2.13
Libs: -L${libdir} -lpjproject -lcrypto -lssl -lasound -lopus
Cflags: -I${includedir} -DPJ_AUTOCONF=1 -DPJ_IS_BIG_ENDIAN=0 -DPJ_IS_LITTLE_ENDIAN=1
```

This is an example from my build machine. You need the correct path for `prefix`.

Once that is in place you can start building libphone. I prefer building out of the source tree, so from inside your libphone directory go up on level.

```shell
cmake -Slibphone -Bbuild-libphone -GNinja -DCMAKE_PREFIX_PATH=$(echo ~)/installed/pjproject -DCMAKE_C_FLAGS="-fPIC" -DCMAKE_CXX_FLAGS="-fPIC" -DCMAKE_INSTALL_PREFIX=$(echo ~)/installed/libphone --log-level=DEBUG
cmake --build build-libphone --config Release --target install
```


## Build instructions for macOS

You can use my already prebuild package `pjproject-apple-platforms`. Install it with the following commands:

```shell
brew tap oliverepper/made
brew install pjproject-apple-platforms
```

libphone can now be build with the following commands:

```shell
cmake -Slibphone -Bbuild-libphone -GNinja -DCMAKE_INSTALL_PREFIX=$(echo ~)/installed/libphone --log-level=DEBUG
cmake --build build-libphone --config Release --target install
```
Binary file added python_cli.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
add_subdirectory(phone)
add_subdirectory(c_cli)
add_subdirectory(cpp_cli)
add_subdirectory(python_cli)
2 changes: 2 additions & 0 deletions src/c_cli/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
add_executable(c_cli main.c)
target_link_libraries(c_cli PRIVATE phone_sharedlib)
127 changes: 127 additions & 0 deletions src/c_cli/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#include <phone.h>
#include <stdio.h>
#include <string.h>
#include <limits.h>

void die(phone_t instance) {
phone_destroy(instance);
fprintf(stderr, "%s\n", phone_last_error());
exit(EXIT_FAILURE);
}

struct app_state {
int last_call_id;
};

void on_incoming_call(int call_id, __attribute__((unused)) void *ctx) {
printf("Incoming call: %d\n", call_id);
}

void on_call_state(int call_id, int state, void *ctx) {
char buffer[64];
phone_state_name(buffer, sizeof(buffer), state);
struct app_state *s = (struct app_state*)ctx;
s->last_call_id = call_id;
printf("Call %d – state: %s\n", call_id, buffer);
}

void clear_input_buffer(void) {
int c;
while ((c = getchar() != '\n') && c != EOF);
}

int read_int(int *in) {
char input[11];
long value;
char *endptr;

clear_input_buffer();
if (fgets(input, sizeof(input), stdin) == NULL) {
fprintf(stderr, "could not read from stdin");
return 1;
}

input[strcspn(input, "\n")] = '\0';

value = strtol(input, &endptr, sizeof(input));
if (*input == '\0' || *endptr != '\0') {
fprintf(stderr, "invalid input.\n");
return 1;
}

if (value <= INT_MAX && value >= INT_MIN) {
*in = (int)value;
} else {
fprintf(stderr, "invalid input.\n");
return 1;
}
return 0;
}

int main() {
struct app_state *state = malloc(sizeof(struct app_state));
state->last_call_id = -1;

const char *nameserver[] = {"217.237.148.22", "217.237.150.51"};
const char *stunserver[] = {"stun.t-online.de"};
phone_t phone = phone_create("Cli Phone in C", nameserver, 2, stunserver, 1);
if (!phone)
die(phone);
phone_set_log_level(0);

phone_register_on_incoming_call_callback(phone, on_incoming_call, NULL);
phone_register_on_call_state_callback(phone, on_call_state, state);

if (phone_configure_opus(phone, 1, 8, 16000) != PHONE_STATUS_SUCCESS)
die(phone);
if (phone_connect(phone,
"tel.t-online.de",
"+4965191899543", NULL) != PHONE_STATUS_SUCCESS)
die(phone);

int command;
char uri[128];
int call_id, level;
do {
printf("last call id: %d\n> ", state->last_call_id);
command = getchar();
switch (command) {
case 'c':
printf("please enter number: ");
clear_input_buffer();
if (fgets(uri, sizeof(uri), stdin) == NULL) {
fprintf(stderr, "could not read number\n");
break;
}
uri[strcspn(uri, "\n")] = '\0';
if (phone_make_call(phone, uri) != PHONE_STATUS_SUCCESS) die(phone);
break;
case 'C':
if (phone_make_call(phone, "+4915123595397") != PHONE_STATUS_SUCCESS) die(phone);
break;
case 'a':
printf("please enter call id: ");
if (read_int(&call_id) != 0) break;
if (phone_answer_call(phone, call_id) != PHONE_STATUS_SUCCESS) die(phone);
break;
case 'h':
printf("please enter call id: ");
if (read_int(&call_id) != 0) break;
if (phone_hangup_call(phone, call_id) != PHONE_STATUS_SUCCESS) die(phone);
break;
case 'H':
phone_hangup_calls(phone);
break;
case 'l':
printf("please enter new log level 0..6: ");
if (read_int(&level) != 0) break;
phone_set_log_level(level);
break;
default:
break;
}
} while (command != 'q' && command != EOF);
printf("shutting down...\n");
phone_destroy(phone);
return 0;
}
2 changes: 2 additions & 0 deletions src/cpp_cli/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
add_executable(cpp_cli main.cpp)
target_link_libraries(cpp_cli PRIVATE phone_sharedlib)
Loading

0 comments on commit f6a212a

Please sign in to comment.