~ehmry/nim_tox

fbcbdea644c994ab23cb0f5407f50207ec8470f7 — Emery Hemingway 10 months ago a0e0200 v0.3.0
Add toxcore/av library
4 files changed, 476 insertions(+), 26 deletions(-)

M src/toxcore.nim
A src/toxcore/av.nim
M tests/test1.nim
M toxcore.nimble
M src/toxcore.nim => src/toxcore.nim +0 -17
@@ 1,22 1,5 @@
# SPDX-FileCopyrightText: 2019 Emery Hemingway
#
# SPDX-License-Identifier: GPL-3.0-or-later
#
#  This file is part of Tox, the free peer to peer instant messenger.
#
#  Tox is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  Tox is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with Tox.  If not, see <http://www.gnu.org/licenses/>.
#

import std/strutils


A src/toxcore/av.nim => src/toxcore/av.nim +456 -0
@@ 0,0 1,456 @@
# SPDX-FileCopyrightText: 2020 Emery Hemingway
# SPDX-License-Identifier: GPL-3.0-or-later

import ../toxcore

{.pragma: ctoxHeader, header: "tox/toxav.h".}
{.pragma: ctoxError, ctoxHeader, importc: "Toxav_$1".}
{.pragma: ctoxEnum, ctoxHeader, importc: "TOXAV_$1".}
{.pragma: ctoxProc, ctoxHeader, importc: "toxav_$1".}
{.pragma: ctoxCallbackProc, ctoxHeader, importc: "toxav_callback_$1".}
{.pragma: ctoxConst, ctoxHeader, importc: "TOXAV_$1".}

template ctoxAssert(good, value: untyped) =
  if value != good: raise newException(ToxError, $value)

type
  Samples* = ptr UncheckedArray[int16]
    ## A unchecked buffer of PCM audio samples.
    ## 960 samples is a typical buffer size.

  CbCall* = proc (friend: Friend; audioEnabled, videoEnabled: bool) {.gcsafe.}
    ## The function type for the call callback.

  CbCallState* = proc (friend: Friend; state: uint32) {.gcsafe.}
    ## The function type for the call_state callback.

  CbAudioBitRate* = proc (friend: Friend; audioBitRate: int) {.gcsafe.}
    ## The function type for the audio_bit_rate callback. The event is triggered
    ## when the network becomes too saturated for current bit rates at which
    ## point core suggests new bit rates.

  CbVideoBitRate* = proc (
    friend: Friend; videoBitRate: int) {.gcsafe.}
    ## The function type for the video_bit_rate callback. The event is triggered
    ## when the network becomes too saturated for current bit rates at which
    ## point core suggests new bit rates.

  CbAudioReceiveFrame* = proc (
    friend: Friend; pcm: Samples; sampleCount: int;
    channels: uint8; samplingRate: uint32) {.gcsafe.}
    ## The function type for the audio_receive_frame callback. The callback can
    ## be called multiple times per single iteration depending on the amount of
    ## queued frames in the buffer. The received format is the same as in send
    ## function.

  CbVideoReceiveFrame* = proc (
    friend: Friend; width, height: uint16;
    y, u, v: ptr uint8; ystride, ustride, vstride: int32) {.gcsafe.}
    ## The function type for the video_receive_frame callback.
    ##
    ## The size of plane data is derived from width and height as documented
    ## below.
    ##
    ## Strides represent padding for each plane that may or may not be present.
    ## You must handle strides in your image processing code. Strides are
    ## negative if the image is bottom-up hence why you MUST ``abs`` it when
    ## calculating plane buffer size.

  CbGroupAudio* = proc (
    group: Conference; peer: Peer; pcm: Samples;
    samples: int; channels: uint8; sampleRate: uint32) {.gcsafe.}

  AvCore = distinct pointer
    ## Opaque C type wrapped by the `ToxAV` object.

  ToxAV* = ref object
    ## Tox A/V instance
    core: AvCore
    call: CbCall
    callState: CbCallState
    audioBitRate: CbAudioBitRate
    videoBitRate: CbVideoBitRate
    audioReceiveFrame: CbAudioReceiveFrame
    videoReceiveFrame: CbVideoReceiveFrame
    groupAudio: CbGroupAudio

template callbackSetter(name, body: untyped) =
  ## Magic for installing Nim closure callbacks into c-toxcore.
  proc `on name`*(av: ToxAV; cb: `Cb name`) =
    template callThru(args: varargs[untyped]) =
      let av = cast[ToxAV](user_data)
      if not av.name.isNil: av.name(args)
    body
    proc name(core: AvCore; callback: cbType; userData: ToxAV) {.ctoxCallbackProc.}
    av.name = cb
    if cb.isNil: `name`(av.core, nil, nil)
    else: `name`(av.core, wrapper, av)

callbackSetter call:
  type cbType = proc (
      av: AvCore; friend: Friend; audioEnabled, videoEnabled: bool;
          user_data: pointer) {.cdecl.}
  proc wrapper(
      av: AvCore; friend: Friend; audioEnabled, videoEnabled: bool;
          user_data: pointer) {.cdecl.} =
    callThru(friend, audioEnabled, videoEnabled)

callbackSetter call_state:
  type cbType = proc (
      av: AvCore; friend: Friend; state: uint32; userData: pointer) {.cdecl.}
  proc wrapper(
      av: AvCore; friend: Friend; state: uint32; userData: pointer) {.cdecl.} =
    callThru(friend, state)

callbackSetter audio_bit_rate:
  type cbType = proc (av: AvCore; friend: Friend; bitRate: uint32;
      userData: pointer) {.cdecl.}
  proc wrapper(av: AvCore; friend: Friend; bitRate: uint32;
      userData: pointer) {.cdecl.} =
    callThru(friend, (int)bitRate)

callbackSetter video_bit_rate:
  type cbType = proc (av: AvCore; friend: Friend; bitRate: uint32;
      userData: pointer) {.cdecl.}
  proc wrapper(av: AvCore; friend: Friend; bitRate: uint32;
      userData: pointer) {.cdecl.} =
    callThru(friend, (int)bitRate)

callbackSetter audio_receive_frame:
  type cbType = proc (av: AvCore; friend: Friend; pcm: ptr int16;
      sampleCount: csize_t; channels: uint8; samplingRate: uint32;
      userData: pointer) {.cdecl.}
  proc wrapper(av: AvCore; friend: Friend; pcm: ptr int16; sampleCount: csize_t;
      channels: uint8; samplingRate: uint32; userData: pointer) {.cdecl.} =
    callThru(friend, cast[Samples](pcm), (int)sampleCount, channels, samplingRate)

callbackSetter video_receive_frame:
  type cbType = proc (av: AvCore; friend: Friend;
      width, height: uint16; y, u, v: ptr uint8;
      ystride, ustride, vstride: int32; userData: pointer) {.cdecl.}
  proc wrapper(av: AvCore; friend: Friend;
      width, height: uint16; y, u, v: ptr uint8;
      ystride, ustride, vstride: int32; userData: pointer) {.cdecl.} =
    callThru(friend, width, height, y, u, v, ystride, ustride, vstride)

callbackSetter group_audio:
  type cbType = proc (av: AvCore; group: Conference; peer: Peer; pcm: ptr int16;
      samples: uint32; channels: uint8; sampleRate: uint32;
      userData: pointer) {.cdecl.}
  proc wrapper(av: AvCore; group: Conference; peer: Peer; pcm: ptr int16;
      samples: uint32; channels: uint8; sampleRate: uint32;
      userData: pointer) {.cdecl.} =
    callThru(group, peer, cast[Samples](pcm), (int)samples, channels, sampleRate)

type Err_New* {.ctoxError.} = enum
  TOXAV_ERR_NEW_OK,
    ## The function returned successfully.
  TOXAV_ERR_NEW_NULL,
    ## One of the arguments to the function was NULL when it was not expected.
  TOXAV_ERR_NEW_MALLOC,
    ## Memory allocation failure while trying to allocate structures required
    ## for the A/V session.
  TOXAV_ERR_NEW_MULTIPLE
    ## Attempted to create a second session for the same Tox instance.

proc newAv*(tox: Tox): ToxAV =
  ## Start new A/V session. There can only be only one session per Tox instance.
  proc toxav_new(tox: Core; error: ptr Err_New): AvCore {.
    importc, ctoxHeader.}
  var err: ERR_NEW
  result = ToxAv(core: toxav_new(tox.core, addr err))
  ctoxAssert(TOXAV_ERR_NEW_OK, err)

proc kill*(av: ToxAV) =
  proc kill(av: AvCore) {.ctoxProc.}
  kill(av.core)
  ## Releases all resources associated with the A/V session.
  ##
  ## If any calls were ongoing, these will be forcibly terminated without
  ## notifying peers. After calling this function, no other functions may be
  ## called and the av pointer becomes invalid.

proc toxav_get_tox(av: AvCore): Core {.ctoxProc.}
  ## Returns the Tox instance the A/V object was created for.

proc iterationInterval*(av: ToxAV): int =
  ## Returns the interval in milliseconds when the next ``iterate`` call
  ## should be. If no call is active at the moment, this function returns 200.
  proc iteration_interval(av: AvCore): uint32 {.ctoxProc.}
  (int)iterationInterval(av.core)

proc iterate*(av: ToxAV) =
  ## Main loop for the session. This function needs to be called in intervals of
  ## ``iterationInterval`` milliseconds. It is best called in the separate
  ## thread from tox_iterate.
  proc iterate(av: AvCore) {.ctoxProc.}
  iterate(av.core)


type Err_Call* {.ctoxError.} = enum
  TOXAV_ERR_CALL_OK,
    ## The function returned successfully.
  TOXAV_ERR_CALL_MALLOC,
    ## A resource allocation error occurred while trying to create the
    ## structures required for the call.
  TOXAV_ERR_CALL_SYNC,
    ## Synchronization error occurred.
  TOXAV_ERR_CALL_FRIEND_NOT_FOUND,
    ## The friend number did not designate a valid friend.
  TOXAV_ERR_CALL_FRIEND_NOT_CONNECTED,
    ## The friend was valid, but not currently connected.
  TOXAV_ERR_CALL_FRIEND_ALREADY_IN_CALL,
    ## Attempted to call a friend while already in an audio or video call with
    ## them.
  TOXAV_ERR_CALL_INVALID_BIT_RATE
    ## Audio or video bit rate is invalid.

proc call*(av: ToxAV; friend: Friend; audioBitRate = 0; videoBitRate = 0): bool =
  ## Call a friend. This will start ringing the friend.
  ##
  ## It is the client's responsibility to stop ringing after a certain timeout,
  ## if such behaviour is desired. If the client does not stop ringing, the
  ## library will not stop until the friend is disconnected. Audio and video
  ## receiving are both enabled by default.
  proc call(av: AvCore; friend: Friend; audio_bit_rate: uint32;
      video_bit_rate: uint32; error: ptr Err_Call): bool {.ctoxProc.}
  var err: Err_Call
  result = call(av.core, friend, (uint32)audioBitRate, (uint32)videoBitRate, addr err)
  ctoxAssert(TOXAV_ERR_CALL_OK, err)


type Err_Answer* {.ctoxError.} = enum
  TOXAV_ERR_ANSWER_OK,
    ## The function returned successfully.
  TOXAV_ERR_ANSWER_SYNC,
    ## Synchronization error occurred.
  TOXAV_ERR_ANSWER_CODEC_INITIALIZATION,
    ## Failed to initialize codecs for call session. Note that codec initiation
    ## will fail if there is no receive callback registered for either audio or
    ## video.
  TOXAV_ERR_ANSWER_FRIEND_NOT_FOUND,
    ## The friend number did not designate a valid friend.
  TOXAV_ERR_ANSWER_FRIEND_NOT_CALLING,
    ## The friend was valid, but they are not currently trying to initiate a call.
    ## This is also returned if this client is already in a call with the friend.
  TOXAV_ERR_ANSWER_INVALID_BIT_RATE
    ## Audio or video bit rate is invalid.

proc answer*(av: ToxAV; friend: Friend;
    audioBitRate = 0;
    videoBitRate = 0): bool =
  ## Accept an incoming call.
  ##
  ## If answering fails for any reason, the call will still be pending and it is
  ## possible to try and answer it later. Audio and video receiving are both
  ## enabled by default.
  assert(av.audioReceiveFrame != nil or av.videoReceiveFrame != nil)
  proc answer(
    av: AvCore; friend: Friend; audio_bit_rate: uint32;
    video_bit_rate: uint32; error: ptr Err_Answer): bool {.ctoxProc.}
  var err: Err_Answer
  result = answer(av.core, friend, audioBitRate.uint32, videoBitRate.uint32, addr err)
  ctoxAssert(TOXAV_ERR_ANSWER_OK, err)

var
  FRIEND_CALL_STATE_NONE* {.ctoxConst.}: uint32
    ## The empty bit mask. None of the bits specified below are set.
  FRIEND_CALL_STATE_ERROR* {.ctoxConst.}: uint32
    ## Set by the AV core if an error occurred on the remote end or if friend
    ## timed out. This is the final state after which no more state
    ## transitions can occur for the call. This call state will never be triggered
    ## in combination with other call states.
  FRIEND_CALL_STATE_FINISHED* {.ctoxConst.}: uint32
    ## The call has finished. This is the final state after which no more state
    ## transitions can occur for the call. This call state will never be
    ## triggered in combination with other call states.
  FRIEND_CALL_STATE_SENDING_A* {.ctoxConst.}: uint32
    ## The flag that marks that friend is sending audio.
  FRIEND_CALL_STATE_SENDING_V* {.ctoxConst.}: uint32
    ## The flag that marks that friend is sending video.
  FRIEND_CALL_STATE_ACCEPTING_A* {.ctoxConst.}: uint32
    ## The flag that marks that friend is receiving audio.
  FRIEND_CALL_STATE_ACCEPTING_V* {.ctoxConst.}: uint32
    ## The flag that marks that friend is receiving video.

type
  CALL_CONTROL* {.ctoxEnum.} = enum
    ## Resume a previously paused call. Only valid if the pause was caused by this
    ## client, if not, this control is ignored. Not valid before the call is accepted.
    TOXAV_CALL_CONTROL_RESUME,
      ## The function returned successfully.
    TOXAV_CALL_CONTROL_PAUSE,
      ## Put a call on hold. Not valid before the call is accepted.
    TOXAV_CALL_CONTROL_CANCEL,
      ## Reject a call if it was not answered, yet. Cancel a call after it was
      ## answered.
    TOXAV_CALL_CONTROL_MUTE_AUDIO,
      ## Request that the friend stops sending audio. Regardless of the friend's
      ## compliance, this will cause the audio_receive_frame event to stop being
      ## triggered on receiving an audio frame from the friend.
    TOXAV_CALL_CONTROL_UNMUTE_AUDIO,
      ## Calling this control will notify client to start sending audio again.
    TOXAV_CALL_CONTROL_HIDE_VIDEO,
      ## Request that the friend stops sending video. Regardless of the friend's
      ## compliance, this will cause the video_receive_frame event to stop being
      ## triggered on receiving a video frame from the friend.
    TOXAV_CALL_CONTROL_SHOW_VIDEO
      ## Calling this control will notify client to start sending video again.

type Err_Call_Control* {.ctoxError.} = enum
  TOXAV_ERR_CALL_CONTROL_OK,
    ## The function returned successfully.
  TOXAV_ERR_CALL_CONTROL_SYNC,
    ## Synchronization error occurred.
  TOXAV_ERR_CALL_CONTROL_FRIEND_NOT_FOUND,
    ## The friend passed did not designate a valid friend.
  TOXAV_ERR_CALL_CONTROL_FRIEND_NOT_IN_CALL,
    ## This client is currently not in a call with the friend. Before the call is
    ## answered, only CANCEL is a valid control.
  TOXAV_ERR_CALL_CONTROL_INVALID_TRANSITION
    ## Happens if user tried to pause an already paused call or if trying to
    ## resume a call that is not paused.

proc callControl*(av: ToxAV; friend: Friend; control: CallControl): bool =
  ## Sends a call control command to a friend.
  proc call_control(
    av: AvCore; friend: Friend; control: CALL_CONTROL;
    error: ptr Err_Call_Control): bool {.ctoxProc.}
  var err: Err_Call_Control
  result = call_control(av.core, friend, control, addr err)
  ctoxAssert(TOXAV_ERR_CALL_CONTROL_OK, err)


type Err_Bit_Rate_Set* {.ctoxError.} = enum
  TOXAV_ERR_BIT_RATE_SET_OK,
    ## The function returned successfully.
  TOXAV_ERR_BIT_RATE_SET_SYNC,
    ## Synchronization error occurred.
  TOXAV_ERR_BIT_RATE_SET_INVALID_BIT_RATE,
    ## The bit rate passed was not one of the supported values.
  TOXAV_ERR_BIT_RATE_SET_FRIEND_NOT_FOUND,
    ## The friend passed did not designate a valid friend.
  TOXAV_ERR_BIT_RATE_SET_FRIEND_NOT_IN_CALL
    ## This client is currently not in a call with the friend.


type Err_Send_Frame* {.ctoxError.} = enum
  TOXAV_ERR_SEND_FRAME_OK,
    ## The function returned successfully.
  TOXAV_ERR_SEND_FRAME_NULL,
    ## In case of video, one of Y, U, or V was NULL. In case of audio, the samples
    ## data pointer was NULL.
  TOXAV_ERR_SEND_FRAME_FRIEND_NOT_FOUND,
    ## The friend passed did not designate a valid friend.
  TOXAV_ERR_SEND_FRAME_FRIEND_NOT_IN_CALL,
    ## This client is currently not in a call with the friend.
  TOXAV_ERR_SEND_FRAME_SYNC,
    ## Synchronization error occurred.
  TOXAV_ERR_SEND_FRAME_INVALID,
    ## One of the frame parameters was invalid. E.g. the resolution may be too
    ## small or too large, or the audio sampling rate may be unsupported.
  TOXAV_ERR_SEND_FRAME_PAYLOAD_TYPE_DISABLED,
    ## Either friend turned off audio or video receiving or we turned off sending
    ## for the said payload.
  TOXAV_ERR_SEND_FRAME_RTP_FAILED
    ## Failed to push frame through rtp interface.


proc audioSendFrame*(
    av: ToxAv; friend: Friend; pcm: ptr int16;
    sampleCount: csize_t; channels: uint8;
    samplingRate: uint32; ): bool =
  ## Send an audio frame to a friend.
  ##
  ## The expected format of the PCM data is: [s1c1][s1c2][...][s2c1][s2c2][...]...
  ## Meaning: sample 1 for channel 1, sample 1 for channel 2, ...
  ## For mono audio, this has no meaning, every sample is subsequent. For stereo,
  ## this means the expected format is LRLRLR... with samples for left and right
  ## alternating.
  ##
  ## Valid number of samples here are ((sample rate) * (audio length) / 1000),
  ## where audio length can be 2.5, 5, 10, 20, 40 or 60 millseconds.
  ##
  ## Supported number of channels are 1 and 2.
  ##
  ## Valid sampling rates are 8000, 12000, 16000, 24000, or 48000.
  proc audio_send_frame(
    av: AvCore; friend: Friend; pcm: ptr int16;
    sample_count: csize_t; channels: uint8;
    sampling_rate: uint32;
    error: ptr Err_Send_Frame): bool {.ctoxProc.}
  var err: Err_Send_Frame
  result = audio_send_frame(av.core, friend, pcm, sampleCount, channels,
      samplingRate, addr err)
  ctoxAssert(TOXAV_ERR_SEND_FRAME_OK, err)


proc audioSetBitRate*(av: ToxAv; friend: Friend; bitRate: uint32): bool =
  ## Set the bit rate to be used in subsequent video frames.
  ## Bit rate is in Kb/sec. Set to 0 to disable.
  proc audio_set_bit_rate(
    av: AvCore; friend: Friend;
    bit_rate: uint32; error: ptr Err_Bit_Rate_Set): bool {.ctoxProc.}
  var err: Err_Bit_Rate_Set
  result = audio_set_bit_rate(av.core, friend, bitRate, addr err)
  ctoxAssert(TOXAV_ERR_BIT_RATE_SET_OK, err)


proc videoSendFrame*(
    av: ptr ToxAV; friend: Friend;
    width, height: uint16; y, u, v: ptr uint8): bool =
  ## Send a video frame to a friend.
  ##
  ## Y - plane should be of size: height * width
  ##
  ## U - plane should be of size: (height/2) * (width/2)
  ##
  ## V - plane should be of size: (height/2) * (width/2)
  proc video_send_frame(
      av: AvCore; friend: Friend; width: uint16;
      height: uint16; y, u, v: ptr uint8; error: ptr Err_Send_Frame): bool {.ctoxProc.}
  var err: Err_Send_Frame
  result = video_send_frame(av.core, friend, width, height, y, u, v, addr err)
  ctoxAssert(TOXAV_ERR_SEND_FRAME_OK, err)

proc videoSetBitRate*(av: ToxAv; friend: Friend; bitRate: uint32): bool =
  ## Set the bit rate to be used in subsequent video frames.
  ## Bit rate is in Kb/sec. Set to 0 to disable.
  proc video_set_bit_rate(
      av: AvCore; friend: Friend;
      bit_rate: uint32; error: ptr Err_Bit_Rate_Set): bool {.ctoxProc.}
  var err: Err_Bit_Rate_Set
  result = video_set_bit_rate(av.core, friend, bitRate, addr err)
  ctoxAssert(TOXAV_ERR_BIT_RATE_SET_OK, err)

proc groupSendAudio*(
    av: ToxAv; group: Conference; pcm: ptr int16;
    samples: int; channels: uint8; sampleRate: uint32): bool =
  ## Send audio to the group chat.
  ##
  ## Note that total size of pcm in bytes is equal to
  ## (samples * channels * sizeof(int16)).
  ##
  ## Valid number of samples are ((sample rate) * (audio length (Valid ones are:
  ## 2.5, 5, 10, 20, 40 or 60 ms)) / 1000)
  ## Valid number of channels are 1 or 2.
  ## Valid sample rates are 8000, 12000, 16000, 24000, or 48000.
  ##
  ## Recommended values are: samples = 960, channels = 1, sample_rate = 48000
  proc group_send_audio(
      tox: AvCore; group: Conference; pcm: ptr int16;
      samples: cuint; channels: uint8; sample_rate: uint32): cint {.ctoxProc.}
  let res = group_send_audio(av.core, group, pcm, (cuint)samples, channels, sampleRate)
  res == 0


proc groupchatDisableAv*(av: ToxAv; group: Conference): bool =
  ## Disable A/V in a groupchat.
  proc groupchat_disable_av(tox: AvCore; group: Conference): cint {.ctoxProc.}
  let res = groupchat_disable_av(av.core, group)
  res == 0

proc groupchatAvEnabled*(av: ToxAv; group: Conference): bool =
  ## Return whether A/V is enabled in the groupchat.
  proc groupchat_av_enabled(tox: AvCore; group: Conference): bool {.ctoxProc.}
  groupchat_av_enabled(av.core, group)

M tests/test1.nim => tests/test1.nim +18 -7
@@ 1,11 1,22 @@
import unittest

import toxcore
import toxcore, toxcore/av

test "version":
  check versionIsCompatible()
suite "Tox Core":
  test "version":
    check versionIsCompatible()

test "newTox":
  let tox = newTox()
  check(tox.address.isValid)
  close tox
  test "newTox":
    let tox = newTox()
    check(tox.address.isValid)
    iterate tox
    close tox

suite "Tox AV":

  test "newAv":
    let
      tox = newTox()
      av = newAv(tox)
    iterate av
    kill av

M toxcore.nimble => toxcore.nimble +2 -2
@@ 1,6 1,6 @@
# Package

version       = "0.2.0"
version       = "0.3.0"
author        = "Emery Hemingway"
description   = "C Tox core wrapper"
license       = "GPL-3.0-or-later"


@@ 9,7 9,7 @@ srcDir        = "src"

# Dependencies

requires "nim >= 1.0.2"
requires "nim >= 1.2.0"

import distros
if detectOs(NixOS):