diff:
@@ -230,3 +230,68 @@ function teardown() {
[ "$status" -eq 0 ]
[[ "${output}" == *"ponG Ping"* ]]
}
+
+@test "checkpoint and restore in external network namespace" {
+ # XXX: currently criu require root containers.
+ requires criu root
+
+ # check if external_net_ns is supported; only with criu 3.10++
+ run ${CRIU} check --feature external_net_ns
+ if [ "$status" -eq 1 ]; then
+ # this criu does not support external_net_ns; skip the test
+ skip "this criu does not support external network namespaces"
+ fi
+
+ # create a temporary name for the test network namespace
+ tmp=`mktemp`
+ rm -f $tmp
+ ns_name=`basename $tmp`
+ # create network namespace
+ ip netns add $ns_name
+ ns_path=`ip netns add $ns_name 2>&1 | sed -e 's/.*"\(.*\)".*/\1/'`
+
+ ns_inode=`ls -iL $ns_path | awk '{ print $1 }'`
+
+ # not necessary with criu 3.10 any more
+ sed -i 's;"readonly": true;"readonly": false;' config.json
+ # tell runc which network namespace to use
+ sed -i "s;\"type\": \"network\";\"type\": \"network\",\"path\": \"$ns_path\";" config.json
+
+ runc run -d --console-socket $CONSOLE_SOCKET test_busybox
+ [ "$status" -eq 0 ]
+
+ testcontainer test_busybox running
+
+ for i in `seq 2`; do
+ # checkpoint the running container; this automatically tells CRIU to
+ # handle the network namespace defined in config.json as an external
+ runc --criu "$CRIU" checkpoint --work-path ./work-dir test_busybox
+ ret=$?
+ # if you are having problems getting criu to work uncomment the following dump:
+ #cat /run/opencontainer/containers/test_busybox/criu.work/dump.log
+ cat ./work-dir/dump.log | grep -B 5 Error || true
+ [ "$ret" -eq 0 ]
+
+ # after checkpoint busybox is no longer running
+ runc state test_busybox
+ [ "$status" -ne 0 ]
+
+ # restore from checkpoint; this should restore the container into the existing network namespace
+ runc --criu "$CRIU" restore -d --work-path ./work-dir --console-socket $CONSOLE_SOCKET test_busybox
+ ret=$?
+ cat ./work-dir/restore.log | grep -B 5 Error || true
+ [ "$ret" -eq 0 ]
+
+ # busybox should be back up and running
+ testcontainer test_busybox running
+
+ # container should be running in same network namespace as before
+ pid=`__runc state test_busybox | jq '.pid'`
+ ns_inode_new=`readlink /proc/$pid/ns/net | sed -e 's/.*\[\(.*\)\]/\1/'`
+ echo "old network namespace inode $ns_inode"
+ echo "new network namespace inode $ns_inode_new"
+ [ "$ns_inode" -eq "$ns_inode_new" ]
+ done
+ ip netns del $ns_name
+}
+

repo: opencontainers/runc

original_message:
tests: add external network namespace tests

This adds a new CRIU based checkpoint/restore test to check if
the restored container runs in the same network namespace as before.

Signed-off-by: Adrian Reber <areber@redhat.com>


change_type: MODIFY

diff:
@@ -1197,7 +1197,7 @@ func (c *linuxContainer) Restore(process *Process, criuOpts *CriuOpts) error {
netns, err := os.Open(nsPath)
defer netns.Close()
if err != nil {
- logrus.Error("If a specific network namespace is defined it must exist: %s", err)
+ logrus.Errorf("If a specific network namespace is defined it must exist: %s", err)
return fmt.Errorf("Requested network namespace %v does not exist", nsPath)
}
inheritFd := new(criurpc.InheritFd)

license: Apache License 2.0

original_message:
Fix travis Go: tip

This fixes

 libcontainer/container_linux.go:1200: Error call has possible formatting directive %s

Signed-off-by: Adrian Reber <areber@redhat.com>


old_path: Dockerfile

new_path: Dockerfile

diff:
@@ -45,14 +45,10 @@ RUN cd /tmp \
&& rm -rf /tmp/bats
# install criu
-# For criu v3.10, a patch is needed for `@test "checkpoint --lazy-pages and restore"`.
-# Starting with v3.11, the patch will no longer be needed.
-# See https://github.com/opencontainers/runc/issues/1863#issuecomment-412074413
-ENV CRIU_VERSION v3.10
+ENV CRIU_VERSION v3.11
RUN mkdir -p /usr/src/criu \
&& curl -sSL https://github.com/checkpoint-restore/criu/archive/${CRIU_VERSION}.tar.gz | tar -v -C /usr/src/criu/ -xz --strip-components=1 \
&& cd /usr/src/criu \
- && curl https://github.com/checkpoint-restore/criu/commit/27034e7c64b00a1f2467afb5ebb1d5b9b1a06ce1.patch | patch -p1 \
&& make install-criu \
&& rm -rf /usr/src/criu

original_message:
Bump CRIU to 3.11

Upgrade CRIU to 3.11 in the Dockerfile as it includes the patch which
was manually added to fix an error with read-only root containers.

Now that the patch is part of the CRIU 3.11 release this simplifies the
Dockerfile (minimal).

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
@@ -15,9 +15,6 @@ function teardown() {
# XXX: currently criu require root containers.
requires criu root
- # criu does not work with external terminals so..
- # setting terminal and root:readonly: to false
-
runc run -d --console-socket $CONSOLE_SOCKET test_busybox
[ "$status" -eq 0 ]
@@ -51,8 +48,8 @@ function teardown() {
# XXX: currently criu require root containers.
requires criu root
+ # The changes to 'terminal' are needed for running in detached mode
sed -i 's;"terminal": true;"terminal": false;' config.json
- sed -i 's;"readonly": true;"readonly": false;' config.json
sed -i 's/"sh"/"sh","-c","for i in `seq 10`; do read xxx || continue; echo ponG $xxx; done"/' config.json
# The following code creates pipes for stdin and stdout.
@@ -75,7 +72,7 @@ function teardown() {
echo -n > $fifo
unlink $fifo
- # run busybox (not detached)
+ # run busybox
__runc run -d test_busybox <&60 >&51 2>&51
[ $? -eq 0 ]
@@ -134,7 +131,9 @@ function teardown() {
skip "this criu does not support lazy migration"
fi
+ # The changes to 'terminal' are needed for running in detached mode
sed -i 's;"terminal": true;"terminal": false;' config.json
+ # This should not be necessary: https://github.com/checkpoint-restore/criu/issues/575
sed -i 's;"readonly": true;"readonly": false;' config.json
sed -i 's/"sh"/"sh","-c","for i in `seq 10`; do read xxx || continue; echo ponG $xxx; done"/' config.json
@@ -252,8 +251,6 @@ function teardown() {
ns_inode=`ls -iL $ns_path | awk '{ print $1 }'`
- # not necessary with criu 3.10 any more
- sed -i 's;"readonly": true;"readonly": false;' config.json
# tell runc which network namespace to use
sed -i "s;\"type\": \"network\";\"type\": \"network\",\"path\": \"$ns_path\";" config.json

original_message:
Small fixes for CRIU based test cases

This removes unnecessary lines from checkpoint.bats like:

 sed -i 's;"readonly": true;"readonly": false;' config.json

and adds (and corrects) comments which are leftover from older
versions of checkpoint.bats.

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
@@ -292,3 +292,53 @@ function teardown() {
ip netns del $ns_name
}
+@test "checkpoint and restore with container specific CRIU config" {
+ # XXX: currently criu require root containers.
+ requires criu root
+
+ tmp=`mktemp /tmp/runc-criu-XXXXXX.conf`
+ # This is the file we write to /etc/criu/default.conf
+ tmplog1=`mktemp /tmp/runc-criu-log-XXXXXX.log`
+ unlink $tmplog1
+ tmplog1=`basename $tmplog1`
+ # That is the actual configuration file to be used
+ tmplog2=`mktemp /tmp/runc-criu-log-XXXXXX.log`
+ unlink $tmplog2
+ tmplog2=`basename $tmplog2`
+ # This adds the annotation 'org.criu.config' to set a container
+ # specific CRIU config file.
+ sed -i "s;\"process\";\"annotations\":{\"org.criu.config\": \"$tmp\"},\"process\";" config.json
+ # Tell CRIU to use another configuration file
+ mkdir -p /etc/criu
+ echo "log-file=$tmplog1" > /etc/criu/default.conf
+ # Make sure the RPC defined configuration file overwrites the previous
+ echo "log-file=$tmplog2" > $tmp
+
+ runc run -d --console-socket $CONSOLE_SOCKET test_busybox
+ [ "$status" -eq 0 ]
+
+ testcontainer test_busybox running
+
+ # checkpoint the running container
+ runc --criu "$CRIU" checkpoint --work-path ./work-dir test_busybox
+ [ "$status" -eq 0 ]
+ ! test -f ./work-dir/$tmplog1
+ test -f ./work-dir/$tmplog2
+
+ # after checkpoint busybox is no longer running
+ runc state test_busybox
+ [ "$status" -ne 0 ]
+
+ test -f ./work-dir/$tmplog2 && unlink ./work-dir/$tmplog2
+ # restore from checkpoint
+ runc --criu "$CRIU" restore -d --work-path ./work-dir --console-socket $CONSOLE_SOCKET test_busybox
+ [ "$status" -eq 0 ]
+ ! test -f ./work-dir/$tmplog1
+ test -f ./work-dir/$tmplog2
+
+ # busybox should be back up and running
+ testcontainer test_busybox running
+ unlink $tmp
+ test -f ./work-dir/$tmplog2 && unlink ./work-dir/$tmplog2
+}
+

original_message:
Added test for container specific CRIU configuration files

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
@@ -18,8 +18,11 @@ checkpointed.
--tcp-established allow open tcp connections
--ext-unix-sk allow external unix sockets
--shell-job allow shell jobs
+ --lazy-pages use userfaultfd to lazily restore memory pages
+ --status-fd value criu writes \0 to this FD once lazy-pages is ready
--page-server value ADDRESS:PORT of the page server
--file-locks handle file locks, for safety
--pre-dump dump container's memory information only, leave the container running after this
--manage-cgroups-mode value cgroups mode: 'soft' (default), 'full' and 'strict'
--empty-ns value create a namespace, but don't restore its properties
+ --auto-dedup enable auto deduplication of memory images

original_message:
Update runc-checkpoint man-page

This just copies the latest output from 'runc checkpoint --help' to the
man page.

Signed-off-by: Adrian Reber <areber@redhat.com>


change_type: ADD

diff:
+# Checkpoint and Restore #
+
+For a basic description about checkpointing and restoring containers with
+`runc` please see [runc-checkpoint(8)](../man/runc-checkpoint.8.md) and
+[runc-restore(8)](../man/runc-restore.8.md).
+
+## Checkpoint/Restore Annotations ##
+
+In addition to specifying options on the command-line like it is described
+in the man-pages (see above), it is also possible to influence CRIU's
+behaviour using CRIU configuration files. For details about CRIU's
+configuration file support please see [CRIU's wiki](https://criu.org/Configuration_files).
+
+In addition to CRIU's default configuration files `runc` tells CRIU to
+also evaluate the file `/etc/criu/runc.conf`. Using the annotation
+`org.criu.config` it is, however, possible to change this additional
+CRIU configuration file.
+
+If the annotation `org.criu.config` is set to an empty string `runc`
+will not pass any additional configuration file to CRIU. With an empty
+string it is therefore possible to disable the additional CRIU configuration
+file. This can be used to make sure that no additional configuration file
+changes CRIU's behaviour accidentally.
+
+If the annotation `org.criu.config` is set to a non-empty string `runc` will
+pass that string to CRIU to be evaluated as an additional configuration file.
+If CRIU cannot open this additional configuration file, it will ignore this
+file and continue.
+
+### Annotation Example to disable additional CRIU configuration file ###
+
+```
+{
+ "ociVersion": "1.0.0",
+ "annotations": {
+ "org.criu.config": ""
+ },
+ "process": {
+```
+
+### Annotation Example to set a specific CRIU configuration file ###
+
+```
+{
+ "ociVersion": "1.0.0",
+ "annotations": {
+ "org.criu.config": "/etc/special-runc-criu-options"
+ },
+ "process": {
+```

original_message:
Add 'org.criu.config' annotation documentation

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
@@ -19,6 +19,7 @@ import (
"syscall" // only for SysProcAttr and Signal
"time"
+ "github.com/cyphar/filepath-securejoin"
"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/criurpc"
@@ -1139,6 +1140,75 @@ func (c *linuxContainer) restoreNetwork(req *criurpc.CriuReq, criuOpts *CriuOpts
}
}
+// makeCriuRestoreMountpoints makes the actual mountpoints for the
+// restore using CRIU. This function is inspired from the code in
+// rootfs_linux.go
+func (c *linuxContainer) makeCriuRestoreMountpoints(m *configs.Mount) error {
+ switch m.Device {
+ case "cgroup":
+ // Do nothing for cgroup, CRIU should handle it
+ case "bind":
+ // The prepareBindMount() function checks if source
+ // exists. So it cannot be used for other filesystem types.
+ if err := prepareBindMount(m, c.config.Rootfs); err != nil {
+ return err
+ }
+ default:
+ // for all other file-systems just create the mountpoints
+ dest, err := securejoin.SecureJoin(c.config.Rootfs, m.Destination)
+ if err != nil {
+ return err
+ }
+ if err := checkMountDestination(c.config.Rootfs, dest); err != nil {
+ return err
+ }
+ m.Destination = dest
+ if err := os.MkdirAll(dest, 0755); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// isPathInPrefixList is a small function for CRIU restore to make sure
+// mountpoints, which are on a tmpfs, are not created in the roofs
+func isPathInPrefixList(path string, prefix []string) bool {
+ for _, p := range prefix {
+ if strings.HasPrefix(path, p+"/") {
+ return false
+ }
+ }
+ return true
+}
+
+// prepareCriuRestoreMounts tries to set up the rootfs of the
+// container to be restored in the same way runc does it for
+// initial container creation. Even for a read-only rootfs container
+// runc modifies the rootfs to add mountpoints which do not exist.
+// This function also creates missing mountpoints as long as they
+// are not on top of a tmpfs, as CRIU will restore tmpfs content anyway.
+func (c *linuxContainer) prepareCriuRestoreMounts(mounts []*configs.Mount) error {
+ // First get a list of a all tmpfs mounts
+ tmpfs := []string{}
+ for _, m := range mounts {
+ switch m.Device {
+ case "tmpfs":
+ tmpfs = append(tmpfs, m.Destination)
+ }
+ }
+ // Now go through all mounts and create the mountpoints
+ // if the mountpoints are not on a tmpfs, as CRIU will
+ // restore the complete tmpfs content from its checkpoint.
+ for _, m := range mounts {
+ if isPathInPrefixList(m.Destination, tmpfs) {
+ if err := c.makeCriuRestoreMountpoints(m); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
func (c *linuxContainer) Restore(process *Process, criuOpts *CriuOpts) error {
c.m.Lock()
defer c.m.Unlock()
@@ -1252,6 +1322,12 @@ func (c *linuxContainer) Restore(process *Process, criuOpts *CriuOpts) error {
}
}
+ // This will modify the rootfs of the container in the same way runc
+ // modifies the container during initial creation.
+ if err := c.prepareCriuRestoreMounts(c.config.Mounts); err != nil {
+ return err
+ }
+
for _, m := range c.config.Mounts {
switch m.Device {
case "bind":

original_message:
Create mountpoints also on restore

runc creates all missing mountpoints when it starts a container, this
commit also creates those mountpoints during restore. Now it is possible
to restore a container using the same, but newly created rootfs just as
during container start.

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
+dist: xenial
language: go
go:
- 1.9.x
@@ -21,9 +22,8 @@ env:
- BUILDTAGS="seccomp apparmor selinux ambient"
before_install:
- - echo "deb http://archive.ubuntu.com/ubuntu trusty-backports main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list
- sudo apt-get -qq update
- - sudo apt-get install -y libseccomp-dev/trusty-backports
+ - sudo apt-get install -y libseccomp-dev
- go get -u golang.org/x/lint/golint
- go get -u github.com/vbatts/git-validation
- env | grep TRAVIS_

original_message:
switched travis to xenial

The CRIU test for lazy migration was always skipped in Travis because
the kernel was too old. This switches Travis testing to dist: xenial
which provides a newer kernel which enables CRIU lazy migration testing.

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
@@ -1015,10 +1015,15 @@ func (c *linuxContainer) Checkpoint(criuOpts *CriuOpts) error {
}
}
- fcg := c.cgroupManager.GetPaths()["freezer"]
- if fcg != "" {
+ if !cgroups.IsCgroup2UnifiedMode() && c.checkCriuVersion(31400) == nil {
+ // CRIU currently cannot handle the v2 freezer correctly
+ // before release 3.14. For older releases we are telling
+ // CRIU to not use the cgroup v2 freezer. CRIU will pause
+ // each process manually using ptrace().
+ if fcg := c.cgroupManager.GetPaths()["freezer"]; fcg != "" {
rpcOpts.FreezeCgroup = proto.String(fcg)
}
+ }
// append optional criu opts, e.g., page-server and port
if criuOpts.PageServer.Address != "" && criuOpts.PageServer.Port != 0 {

original_message:
cgroup2: use CRIU's new freezer v2 support

The newest CRIU version supports freezer v2 and this tells runc
to use it if new enough or fall back to non-freezer based process
freezing on cgroup v2 system.

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
dist: bionic
language: go
+os: linux
go:
- 1.14.x
- 1.13.x
@@ -7,7 +8,7 @@ go:
cache:
directories:
- /home/travis/.vagrant.d/boxes
-matrix:
+jobs:
include:
- go: 1.14.x
name: "verify-dependencies"
@@ -55,7 +56,6 @@ matrix:
go_import_path: github.com/opencontainers/runc
# `make ci` uses Docker.
-sudo: required
services:
- docker

original_message:
Fix .travis.yml warnings

Travis reports following warnings which are fixed with this commit.

   root: deprecated key sudo (The key `sudo` has no effect anymore.)
   root: missing os, using the default linux
   root: key matrix is an alias for jobs, using jobs

Signed-off-by: Adrian Reber <areber@redhat.com>


message: Upgrade go-criu to 4.1.0

diff:
@@ -3,7 +3,7 @@ module github.com/opencontainers/runc
go 1.14
require (
- github.com/checkpoint-restore/go-criu/v4 v4.0.2
+ github.com/checkpoint-restore/go-criu/v4 v4.1.0
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775
github.com/containerd/console v1.0.0
github.com/coreos/go-systemd/v22 v22.0.0

diff:
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/checkpoint-restore/go-criu/v4 v4.0.2 h1:jt+rnBIhFtPw0fhtpYGcUOilh4aO9Hj7r+YLEtf30uA=
-github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
+github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo=
+github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775 h1:cHzBGGVew0ezFsq2grfy2RsB8hO/eNyBgOLHBCqfR1U=
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ=

diff:
language: go
-sudo: required
+dist: bionic
os:
- linux
go:
@@ -13,6 +13,7 @@ env:
install:
- sudo apt-get update
- sudo apt-get install -y libprotobuf-dev libprotobuf-c0-dev protobuf-c-compiler protobuf-compiler python-protobuf libnl-3-dev libnet-dev libcap-dev
+ - make install.tools
- go get github.com/checkpoint-restore/go-criu
- git clone --single-branch -b ${CRIU_BRANCH} https://github.com/checkpoint-restore/criu.git
- cd criu; make
@@ -20,6 +21,8 @@ install:
- cd ..
script:
# This builds the code without running the tests.
- - make build phaul test/test test/phaul test/piggie
+ - make lint build phaul test/test test/phaul test/piggie
# Run actual test as root as it uses CRIU.
- sudo make test phaul-test
+ # This builds crit-go
+ - make -C crit-go/magic-gen lint build magicgen test

diff:
@@ -12,7 +12,7 @@ endif
all: build test phaul phaul-test
lint:
- @golint . test phaul
+ @golint -set_exit_status . test phaul
build:
@$(GO) build -v

diff:
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=

diff:
@@ -16,11 +16,20 @@ import (
type Criu struct {
swrkCmd *exec.Cmd
swrkSk *os.File
+ swrkPath string
}
// MakeCriu returns the Criu object required for most operations
func MakeCriu() *Criu {
- return &Criu{}
+ return &Criu{
+ swrkPath: "criu",
+ }
+}
+
+// SetCriuPath allows setting the path to the CRIU binary
+// if it is in a non standard location
+func (c *Criu) SetCriuPath(path string) {
+ c.swrkPath = path
}
// Prepare sets up everything for the RPC communication to CRIU
@@ -36,7 +45,7 @@ func (c *Criu) Prepare() error {
defer srv.Close()
args := []string{"swrk", strconv.Itoa(fds[1])}
- cmd := exec.Command("criu", args...)
+ cmd := exec.Command(c.swrkPath, args...)
err = cmd.Start()
if err != nil {

diff:
-# github.com/checkpoint-restore/go-criu/v4 v4.0.2
+# github.com/checkpoint-restore/go-criu/v4 v4.1.0
## explicit
github.com/checkpoint-restore/go-criu/v4
github.com/checkpoint-restore/go-criu/v4/rpc

original_message:
Upgrade go-criu to 4.1.0

Signed-off-by: Adrian Reber <areber@redhat.com>


diff:
@@ -315,3 +315,45 @@ function simple_cr() {
unlink "$tmp"
test -f ./work-dir/"$tmplog2" && unlink ./work-dir/"$tmplog2"
}
+
+@test "checkpoint and restore with nested bind mounts" {
+ bind1=$(mktemp -d -p .)
+ bind2=$(mktemp -d -p .)
+ update_config ' .mounts += [{
+ type: "bind",
+ source: "'"$bind1"'",
+ destination: "/test",
+ options: ["rw", "bind"]
+ },
+ {
+ type: "bind",
+ source: "'"$bind2"'",
+ destination: "/test/for/nested",
+ options: ["rw", "bind"]
+ }]'
+
+ runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
+ [ "$status" -eq 0 ]
+
+ testcontainer test_busybox running
+
+ # checkpoint the running container
+ runc --criu "$CRIU" checkpoint --work-path ./work-dir test_busybox
+ grep -B 5 Error ./work-dir/dump.log || true
+ [ "$status" -eq 0 ]
+
+ # after checkpoint busybox is no longer running
+ testcontainer test_busybox checkpointed
+
+ # cleanup mountpoints created by runc during creation
+ # the mountpoints should be recreated during restore - that is the actual thing tested here
+ rm -rf "${bind1:?}"/*
+
+ # restore from checkpoint
+ runc --criu "$CRIU" restore -d --work-path ./work-dir --console-socket "$CONSOLE_SOCKET" test_busybox
+ grep -B 5 Error ./work-dir/restore.log || true
+ [ "$status" -eq 0 ]
+
+ # busybox should be back up and running
+ testcontainer test_busybox running
+}

original_message:
tests: test nested bind mount restore

This adds a checkpoint/restore test to ensure that nested bind mount
mountpoints are correctly re-created.

Signed-off-by: Adrian Reber <areber@redhat.com>


message: give podcast a proper README

diff:
# Podcast iOS Client
+Podcast is another app from [CUAppDev](http://cuappdev.org), a project team at Cornell University. It is a podcast client that seeks to transform the way you listen to, interact with, and share and discover podcast content.
+
+## Development
+### Installation
+We use [CocoaPods](http://cocoapods.org) for our dependency manager. This should be installed before continuing.
+
+After cloning the project, `cd` into the new directory and install dependencies with
+```
+pod install
+```
+Open the Podcast Xcode workspace, `Podcast.xcworkspace`, and enjoy!

license: MIT License

repo: cuappdev/podcast-ios

original_message: give podcast a proper README


message: simplify Player controls and variables

diff:
@@ -11,28 +11,23 @@ import AVFoundation
enum PlayerStatus {
case empty
- case preparingToPlay
+ case loading
case playing
case paused
- case failed
case finished
}
-/// Protocol for the singleton Player to observe changes in the Player's state and time changes
protocol PlayerDelegate: class {
- /// Called when the Player's state changes. Override to observe when the state is changed.
func playerDidChangeState()
- /// Called when the Player has made playback progress. Override to observe when the Player makes progress on playback.
func playerDidUpdateTime()
}
-/// Handles playback of podcasts from URLs
class Player: NSObject {
static let sharedInstance = Player()
private override init() {
playerStatus = .empty
- shouldAutoPlay = false
+ shouldPlayWhenLoaded = true
super.init()
}
@@ -45,7 +40,7 @@ class Player: NSObject {
}
weak var delegate: PlayerDelegate?
- private var shouldAutoPlay: Bool
+ private var shouldPlayWhenLoaded: Bool
private var playerContext: UnsafeMutableRawPointer?
private var timeObserverToken: Any?
private var currentAVPlayer: AVPlayer? {
@@ -69,21 +64,17 @@ class Player: NSObject {
removeEndOfItemObserver()
}
- /// Prepares the player to play a new track from a URL. If the Player is in the .preparingToPlay state, this has no effect on current playback.
- /// - Parameters:
- /// - url: The url to fetch and play from.
func prepareToPlay(url: URL?) {
if let url = url {
- if currentURL != url || playerStatus != .preparingToPlay {
+ if currentURL != url || playerStatus != .loading {
// create a new player if this is a new URL or we aren't currently preparing a URL
- playerStatus = .preparingToPlay
+ playerStatus = .loading
currentAVPlayer = AVPlayer(url: url)
currentAVPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.old, .new], context: &playerContext)
}
}
}
- /// Plays the current track and sets the Player status to .playing if successful
func play() {
guard let player = currentAVPlayer else { return }
player.play()
@@ -92,7 +83,6 @@ class Player: NSObject {
addEndOfItemObserver()
}
- /// Pauses playback of the current track and sets the Player status to .paused if successful
func pause() {
guard let player = currentAVPlayer else { return }
player.pause()
@@ -101,7 +91,6 @@ class Player: NSObject {
removeEndOfItemObserver()
}
- /// Adds a time observer that observes the progress of the player every 1 second
func addTimeObserver() {
guard let player = currentAVPlayer else { return }
timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1.0, Int32(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [weak self] _ in
@@ -109,7 +98,6 @@ class Player: NSObject {
})
}
- /// Removes time observer if there currently is one
func removeTimeObserver() {
guard let token = timeObserverToken, let player = currentAVPlayer else { return }
@@ -125,12 +113,10 @@ class Player: NSObject {
NotificationCenter.default.removeObserver(self)
}
- /// Toggles playback of the Player. If the player is in the preparingToPlay state and cannot yet play,
- /// this toggles whether the player will autoplay once it is ready.
func togglePlaying() {
switch playerStatus {
- case .preparingToPlay:
- shouldAutoPlay = !shouldAutoPlay
+ case .loading:
+ shouldPlayWhenLoaded = !shouldPlayWhenLoaded
case .playing:
pause()
case .paused:
@@ -143,11 +129,8 @@ class Player: NSObject {
}
}
- /// Skips a variable amount of seconds in the current audio track. Skips forward or backward based on a positive or negative seconds value.
- /// - Parameters:
- /// - seconds: the amount of time in seconds to skip. If negative, skips backward.
func skip(seconds: Float64) {
- guard let player = currentAVPlayer, playerStatus != .empty || playerStatus != .preparingToPlay else { return }
+ guard let player = currentAVPlayer, playerStatus != .empty || playerStatus != .loading else { return }
let newTime = CMTimeAdd(player.currentTime(), CMTimeMakeWithSeconds(seconds, Int32(NSEC_PER_SEC)))
// if player had finished and skip is backwards in time, move to a paused state
@@ -159,20 +142,15 @@ class Player: NSObject {
}
- /// Returns the progress for the Player's current track.
- /// - Returns: a Float in the range [0.0,1.0] specifying what the current location of playback is relative to the entire track.
func getProgress() -> Float {
- if playerStatus == .empty || playerStatus == .failed || currentAVPlayer?.currentItem == nil {
+ if playerStatus == .empty || playerStatus == .loading || currentAVPlayer?.currentItem == nil {
return 0.0
}
return Float((currentAVPlayer?.currentItem?.currentTime().seconds)!) / Float((currentAVPlayer?.currentItem?.duration.seconds)!)
}
- /// Sets the progress of the Player's current track
- /// - Parameters:
- /// - progress: the value to set where the Player should begin playback. Should be in range [0.0,1.0], but will be set to 0.0 if less than 0.0 and 1.0 if greater than 1.0
func setProgress(progress: Float) {
- if playerStatus == .empty || playerStatus == .failed {
+ if playerStatus == .empty || playerStatus == .loading {
return
}
@@ -193,12 +171,10 @@ class Player: NSObject {
}
}
- /// - Returns: the duration of the current item in the Player. nil if no item
func getDuration() -> CMTime? {
return currentAVPlayer?.currentItem?.duration
}
- /// - Returns: the time passed if there is a current item in the Player. nil if no item
func getTimePassed() -> CMTime? {
if playerStatus == .finished {
return getDuration()
@@ -206,7 +182,6 @@ class Player: NSObject {
return currentAVPlayer?.currentItem?.currentTime()
}
- /// - Returns: the time left from current playback if there is a current item in the Player. nil if no item
func getTimeLeft() -> CMTime? {
guard let timePassed = getTimePassed(), let duration = getDuration() else {
return nil
@@ -221,6 +196,20 @@ class Player: NSObject {
delegate?.playerDidUpdateTime()
}
+ func shouldDisplayPlayButton() -> Bool {
+ switch playerStatus {
+ case .empty, .finished, .paused:
+ // equivalend to .paused; should indicate that Player instance has paused playback
+ return true
+ case .playing:
+ return false
+ case .loading:
+ // equivalent to .playing; in this case, playback is defined
+ // by whether Player will autoplay once track is loaded
+ return !shouldPlayWhenLoaded
+ }
+ }
+
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard context == &playerContext else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
@@ -239,11 +228,11 @@ class Player: NSObject {
switch status {
case .readyToPlay:
playerStatus = .paused
- if shouldAutoPlay {
+ if shouldPlayWhenLoaded {
togglePlaying()
}
case .failed:
- playerStatus = .failed
+ playerStatus = .empty
default:
break
}

diff:
@@ -105,7 +105,6 @@ class PlayerControlsView: UIView, PlayerDelegate {
addSubview(backwardsLabel)
Player.sharedInstance.delegate = self
- Player.sharedInstance.prepareToPlay(url: URL(string: "http://play.podtrac.com/npr-344098539/npr.mc.tritondigital.com/WAITWAIT_PODCAST/media/anon.npr-podcasts/podcast/344098539/495356606/npr_495356606.mp3?orgId=1&d=2995&p=344098539&story=495356606&t=podcast&e=495356606&ft=pod&f=344098539")!)
}
required init?(coder aDecoder: NSCoder) {
@@ -205,13 +204,10 @@ class PlayerControlsView: UIView, PlayerDelegate {
}
func playerDidChangeState() {
- switch Player.sharedInstance.playerStatus {
- case .playing:
- playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Pause"), for: .normal)
- case .paused, .finished:
+ if Player.sharedInstance.shouldDisplayPlayButton() {
playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Play"), for: .normal)
- default:
- break
+ } else {
+ playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Pause"), for: .normal)
}
}

diff:
@@ -34,4 +34,8 @@ class PlayerViewController: UIViewController {
view.addSubview(artworkImageView)
}
+ override func viewDidAppear(_ animated: Bool) {
+ controlsView.playerDidUpdateTime()
+ controlsView.playerDidChangeState()
+ }
}

original_message: simplify Player controls and variables


message: Move all player ui elements and actions to PlayerVC

diff:
/* Begin PBXBuildFile section */
421501271D9F5FE1002839D1 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421501261D9F5FE1002839D1 /* Player.swift */; };
- 42AEE2521D8A08AA001C4786 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42AEE2511D8A08AA001C4786 /* PlayerControlsView.swift */; };
42D7D98D1D80C557000691A4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D7D98C1D80C557000691A4 /* AppDelegate.swift */; };
42D7D9921D80C557000691A4 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 42D7D9901D80C557000691A4 /* Main.storyboard */; };
42D7D9941D80C557000691A4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42D7D9931D80C557000691A4 /* Assets.xcassets */; };
/* Begin PBXFileReference section */
13C8D9A7A7FB0C3D27C81500 /* Pods-PodcastTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PodcastTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PodcastTests/Pods-PodcastTests.debug.xcconfig"; sourceTree = "<group>"; };
421501261D9F5FE1002839D1 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
- 42AEE2511D8A08AA001C4786 /* PlayerControlsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
42D7D9891D80C557000691A4 /* Podcast.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Podcast.app; sourceTree = BUILT_PRODUCTS_DIR; };
42D7D98C1D80C557000691A4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
42D7D9911D80C557000691A4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
42F684EC1D80CC9F0035217A /* DiscoverViewController.swift */,
7F9D0E8F1D85029C00D2BC1C /* Podcast-Bridging-Header.h */,
7F9D0E961D85A37700D2BC1C /* LoginViewController.swift */,
- 42AEE2511D8A08AA001C4786 /* PlayerControlsView.swift */,
D300C0911DAEF801000CDBF5 /* CategoriesFeedViewController.swift */,
D37D799F1DEF920000D752F0 /* SearchViewController.swift */,
);
7F97FBDE1D8663000073FC0F /* REST.swift in Sources */,
D393D7551D8F0D0C00241460 /* DiscoverTableViewCell.swift in Sources */,
421501271D9F5FE1002839D1 /* Player.swift in Sources */,
- 42AEE2521D8A08AA001C4786 /* PlayerControlsView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

diff:
//
import UIKit
+import CoreMedia
-class PlayerViewController: UIViewController {
+class PlayerViewController: UIViewController, PlayerDelegate {
// Mark: Constants
let PlayerControlPanelHeight: CGFloat = 182
+ let ForwardsBackwardsButtonSize: CGFloat = 44
+ let PlayPauseButtonSize: CGFloat = 40
+ let SliderHeight: CGFloat = 6
+ let SliderInset: CGFloat = 48
+ let SliderCenterY: CGFloat = 78
+ let EpisodeNameLabelYValue: CGFloat = 21
+ let SeriesNameLabelYValue: CGFloat = 40
+ let TimeLabelHorizontalInset: CGFloat = 14
+ let ControlButtonsCenterY: CGFloat = 120
+ let ControlButtonsCenterHorizontalOffset: CGFloat = 55
// Mark: Properties
- var controlsView: PlayerControlsView!
+ var controlsView: UIView!
var artworkImageView: UIImageView!
+ var episodeNameLabel: UILabel!
+ var seriesNameLabel: UILabel!
+ var slider: UISlider!
+ var playPauseButton: UIButton!
+ var forwardsButton: UIButton!
+ var forwardsLabel: UILabel!
+ var backwardsButton: UIButton!
+ var backwardsLabel: UILabel!
+ var rightTimeLabel: UILabel!
+ var leftTimeLabel: UILabel!
+ var isScrubbing = false
+
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .podcastGrayLight
tabBarController?.title = "Now Playing"
- controlsView = PlayerControlsView(frame: CGRect(x: 0, y: view.frame.height - PlayerControlPanelHeight - tabBarController!.tabBar.frame.height, width: view.frame.width, height: PlayerControlPanelHeight))
- view.addSubview(controlsView)
-
artworkImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - PlayerControlPanelHeight - tabBarController!.tabBar.frame.height))
artworkImageView.contentMode = .scaleAspectFill
artworkImageView.image = UIImage(named: "SampleSeriesArtwork")
view.addSubview(artworkImageView)
+
+ controlsView = UIView(frame: CGRect(x: 0, y: view.frame.height - PlayerControlPanelHeight - tabBarController!.tabBar.frame.height, width: view.frame.width, height: PlayerControlPanelHeight))
+ controlsView.backgroundColor = .white
+ view.addSubview(controlsView)
+
+ episodeNameLabel = UILabel(frame: .zero)
+ episodeNameLabel.textAlignment = .center
+ episodeNameLabel.font = .boldSystemFont(ofSize: 15)
+ episodeNameLabel.text = "Episode Name"
+ episodeNameLabel.sizeToFit()
+ episodeNameLabel.center = CGPoint(x: view.frame.width/2, y: EpisodeNameLabelYValue)
+ controlsView.addSubview(episodeNameLabel)
+
+ seriesNameLabel = UILabel(frame: .zero)
+ seriesNameLabel.textAlignment = .center
+ seriesNameLabel.font = .systemFont(ofSize: 13)
+ seriesNameLabel.text = "Series Name"
+ seriesNameLabel.sizeToFit()
+ seriesNameLabel.center = CGPoint(x: view.frame.width/2, y: SeriesNameLabelYValue)
+ controlsView.addSubview(seriesNameLabel)
+
+ slider = UISlider(frame: .zero)
+ slider.thumbTintColor = .podcastGreenBlue
+ slider.minimumTrackTintColor = .podcastBlueLight
+ slider.maximumTrackTintColor = .podcastBlueLight
+ slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
+ slider.addTarget(self, action: #selector(endScrubbing), for: .touchUpInside)
+ slider.addTarget(self, action: #selector(endScrubbing), for: .touchUpOutside)
+ slider.frame = CGRect(x: 0, y: 0, width: view.frame.width - (2 * SliderInset), height: SliderHeight)
+ slider.center = CGPoint(x: view.frame.width/2, y: SliderCenterY)
+ controlsView.addSubview(slider)
+
+ leftTimeLabel = UILabel(frame: .zero)
+ leftTimeLabel.font = .systemFont(ofSize: 12)
+ leftTimeLabel.textAlignment = .center
+ leftTimeLabel.text = "-:--"
+ leftTimeLabel.sizeToFit()
+ leftTimeLabel.center = CGPoint(x: TimeLabelHorizontalInset + leftTimeLabel.frame.width/2, y: SliderCenterY)
+ controlsView.addSubview(leftTimeLabel)
+
+ rightTimeLabel = UILabel(frame: .zero)
+ rightTimeLabel.font = .systemFont(ofSize: 12)
+ rightTimeLabel.textAlignment = .center
+ rightTimeLabel.text = "-:--"
+ rightTimeLabel.sizeToFit()
+ rightTimeLabel.center = CGPoint(x: view.frame.width - rightTimeLabel.frame.width/2 - TimeLabelHorizontalInset, y: SliderCenterY)
+ controlsView.addSubview(rightTimeLabel)
+
+ playPauseButton = UIButton(frame: .zero)
+ playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Play"), for: .normal)
+ playPauseButton.adjustsImageWhenHighlighted = false
+ playPauseButton.addTarget(self, action: #selector(playPauseButtonPress), for: .touchUpInside)
+ playPauseButton.frame = CGRect(x: 0, y: 0, width: PlayPauseButtonSize, height: PlayPauseButtonSize)
+ playPauseButton.center = CGPoint(x: view.frame.width/2, y: ControlButtonsCenterY)
+ controlsView.addSubview(playPauseButton)
+
+ forwardsButton = UIButton(frame: .zero)
+ forwardsButton.setBackgroundImage(#imageLiteral(resourceName: "Forwards"), for: .normal)
+ forwardsButton.adjustsImageWhenHighlighted = false
+ forwardsButton.addTarget(self, action: #selector(forwardButtonPress), for: .touchUpInside)
+ forwardsButton.frame = CGRect(x: 0, y: 0, width: ForwardsBackwardsButtonSize, height: ForwardsBackwardsButtonSize)
+ forwardsButton.center = CGPoint(x: playPauseButton.frame.maxX + ControlButtonsCenterHorizontalOffset, y: ControlButtonsCenterY)
+ controlsView.addSubview(forwardsButton)
+
+ forwardsLabel = UILabel(frame: .zero)
+ forwardsLabel.text = "30"
+ forwardsLabel.sizeToFit()
+ forwardsLabel.center = forwardsButton.center
+ controlsView.addSubview(forwardsLabel)
+
+ backwardsButton = UIButton(frame: .zero)
+ backwardsButton.setBackgroundImage(#imageLiteral(resourceName: "Backwards"), for: .normal)
+ backwardsButton.adjustsImageWhenHighlighted = false
+ backwardsButton.addTarget(self, action: #selector(backwardButtonPress), for: .touchUpInside)
+ backwardsButton.frame = CGRect(x: 0, y: 0, width: ForwardsBackwardsButtonSize, height: ForwardsBackwardsButtonSize)
+ backwardsButton.center = CGPoint(x: playPauseButton.frame.minX - ControlButtonsCenterHorizontalOffset, y: ControlButtonsCenterY)
+ controlsView.addSubview(backwardsButton)
+
+ backwardsLabel = UILabel(frame: .zero)
+ backwardsLabel.text = "15"
+ backwardsLabel.sizeToFit()
+ backwardsLabel.center = backwardsButton.center
+ controlsView.addSubview(backwardsLabel)
+
+ Player.sharedInstance.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
- controlsView.playerDidUpdateTime()
- controlsView.playerDidChangeState()
+ playerDidUpdateTime()
+ playerDidChangeState()
+ }
+
+ func endScrubbing() {
+ Player.sharedInstance.setProgress(progress: slider.value)
+ isScrubbing = false
+ }
+
+ func sliderValueChanged() {
+ isScrubbing = true
+
+ // update time labels to be the slider value
+ guard let duration = Player.sharedInstance.getDuration() else { return }
+
+ let newSeconds = duration.seconds * Double(slider.value)
+ let newTimePassed = CMTime(seconds: newSeconds, preferredTimescale: Int32(NSEC_PER_SEC))
+ let newTimeLeft = CMTimeSubtract(duration, newTimePassed)
+
+ leftTimeLabel.text = newTimePassed.durationText
+ leftTimeLabel.sizeToFit()
+ rightTimeLabel.text = newTimeLeft.durationText
+ rightTimeLabel.sizeToFit()
+ }
+
+
+ func playPauseButtonPress() {
+ Player.sharedInstance.togglePlaying()
+ }
+
+ func forwardButtonPress() {
+ Player.sharedInstance.skip(seconds: 30.0)
+ }
+
+ func backwardButtonPress() {
+ Player.sharedInstance.skip(seconds: -15.0)
+ }
+
+ // Mark: PlayerDelegate
+
+ func playerDidUpdateTime() {
+ // don't update slider value and time labels if currently scrubbing
+ if isScrubbing { return }
+
+ slider.value = Player.sharedInstance.getProgress()
+
+ if let timePassed = Player.sharedInstance.getTimePassed() {
+ leftTimeLabel.text = timePassed.durationText
+ } else {
+ leftTimeLabel.text = "-:--"
+ }
+
+ if let timeLeft = Player.sharedInstance.getTimeLeft() {
+ rightTimeLabel.text = timeLeft.durationText
+ } else {
+ rightTimeLabel.text = "-:--"
+ }
+
+ leftTimeLabel.sizeToFit()
+ rightTimeLabel.sizeToFit()
+ }
+
+ func playerDidChangeState() {
+ if Player.sharedInstance.shouldDisplayPlayButton() {
+ playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Play"), for: .normal)
+ } else {
+ playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Pause"), for: .normal)
+ }
}
}


message: Player now plays episode objects! abstract away from urls

diff:
@@ -260,13 +260,8 @@ class DiscoverTableViewCell: UITableViewCell {
}
func clickToPlayImageButtonPress() {
- if let episode = self.episode {
- if let url = episode.mp3URL {
- Player.sharedInstance.prepareToPlay(url: url)
- Player.sharedInstance.play()
- }
- }
- return
+ guard let episode = episode else { return }
+ Player.sharedInstance.playEpisode(episode: episode)
}
}

diff:
@@ -48,14 +48,7 @@ class Player: NSObject {
oldValue?.pause()
}
}
- private var currentURL: URL? {
- didSet {
- if oldValue != currentURL {
- prepareToPlay(url: currentURL)
- }
- }
- }
-
+ private var currentEpisode: Episode?
deinit {
do {
try currentAVPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status), context: &playerContext)
@@ -64,16 +57,16 @@ class Player: NSObject {
removeEndOfItemObserver()
}
- func prepareToPlay(url: URL?) {
- if let url = url {
- if currentURL != url || playerStatus != .loading {
- // create a new player if this is a new URL or we aren't currently preparing a URL
+ func playEpisode(episode: Episode) {
+ if episode.mp3URL == nil || episode == currentEpisode || playerStatus == .loading {
+ // currently only support switching episodes when Player isn't already loading
+ return
+ }
+ currentEpisode = episode
playerStatus = .loading
- currentAVPlayer = AVPlayer(url: url)
+ currentAVPlayer = AVPlayer(url: episode.mp3URL!)
currentAVPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.old, .new], context: &playerContext)
}
- }
- }
func play() {
guard let player = currentAVPlayer else { return }


message: remove misspelled gitignore

change_type: DELETE

old_path: .gitingore

diff:
-# Xcode
-#
-# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-
-## Build generated
-build/
-DerivedData
-
-## Various settings
-*.pbxuser
-!default.pbxuser
-*.mode1v3
-!default.mode1v3
-*.mode2v3
-!default.mode2v3
-*.perspectivev3
-!default.perspectivev3
-xcuserdata
-.DS_Store
-
-## Other
-*.xccheckout
-*.moved-aside
-*.xcuserstate
-*.xcscmblueprint
-
-## Obj-C/Swift specific
-*.hmap
-*.ipa
-
-# CocoaPods
-#
-# We recommend against adding the Pods directory to your .gitignore. However
-# you should judge for yourself, the pros and cons are mentioned at:
-# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control
-
-Pods/

original_message: remove misspelled gitignore


message: big improvements to player

diff:
@@ -48,9 +48,9 @@ extension UIFont {
extension CMTime {
/// Returns a string representation of the CMTime in the format `h:mm:ss` if time is greater than or equal to one hour, and `mm:ss` if less than one hour
- var durationText: String {
+ var descriptionText: String {
if self.isIndefinite {
- return "-:--"
+ return "0:00"
}
let totalSeconds = Int(self.seconds)
let hours = totalSeconds / 3600

diff:
import UIKit
import AVFoundation
-enum PlayerStatus {
- case empty
- case loading
- case playing
- case paused
- case finished
-}
-
protocol PlayerDelegate: class {
- func playerDidChangeState()
- func playerDidUpdateTime()
+ func updateUI()
}
class Player: NSObject {
-
static let sharedInstance = Player()
private override init() {
- playerStatus = .empty
- shouldPlayWhenLoaded = true
+ player = AVPlayer()
+ autoplay = true
+ currentItemPrepared = false
super.init()
}
- var playerStatus: PlayerStatus {
- didSet {
- if oldValue != playerStatus {
- delegate?.playerDidChangeState()
- }
- }
+ deinit {
+ removeCurrentItemStatusObserver()
+ removeTimeObservers()
}
weak var delegate: PlayerDelegate?
- private var shouldPlayWhenLoaded: Bool
- private var playerContext: UnsafeMutableRawPointer?
+
+ // Mark: KVO variables
+
+ private var playerItemContext: UnsafeMutableRawPointer?
private var timeObserverToken: Any?
- private var currentAVPlayer: AVPlayer? {
+
+ // Mark: Playback variables/methods
+
+ private var player: AVPlayer
+ private(set) var currentEpisode: Episode? {
didSet {
- oldValue?.pause()
+ autoplay = true
+ currentItemPrepared = false
}
}
- private var currentEpisode: Episode?
- deinit {
- do {
- try currentAVPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status), context: &playerContext)
- } catch {}
- removeTimeObserver()
- removeEndOfItemObserver()
+ private var autoplay: Bool
+ private var currentItemPrepared: Bool
+ var isPlaying: Bool {
+ get {
+ return player.rate != 0.0 || (!currentItemPrepared && autoplay && (player.currentItem != nil))
+ }
}
func playEpisode(episode: Episode) {
- if episode.mp3URL == nil || episode == currentEpisode || playerStatus == .loading {
- // currently only support switching episodes when Player isn't already loading
+ if currentEpisode?.id == episode.id {
+ // TODO: decide how to handle this case. do nothing? restart track at beginning?
+ // also, currently all episode ids showing as the same...
+ }
+
+ guard let url = episode.mp3URL else {
+ print("Episode \(episode.title) mp3URL is nil. Unable to play.")
return
}
+
+ if player.status == AVPlayerStatus.failed {
+ if let error = player.error {
+ print(error)
+ }
+ player = AVPlayer()
+ }
+
+ // cleanup any previous AVPlayerItem
+ pause()
+ removeCurrentItemStatusObserver()
+
currentEpisode = episode
- playerStatus = .loading
- currentAVPlayer = AVPlayer(url: episode.mp3URL!)
- currentAVPlayer?.addObserver(self, forKeyPath: #keyPath(AVPlayer.status), options: [.old, .new], context: &playerContext)
+ let asset = AVAsset(url: url)
+ let playerItem = AVPlayerItem(asset: asset,
+ automaticallyLoadedAssetKeys: ["playable"])
+ playerItem.addObserver(self,
+ forKeyPath: #keyPath(AVPlayerItem.status),
+ options: [.old, .new],
+ context: &playerItemContext)
+ player.replaceCurrentItem(with: playerItem)
}
func play() {
- guard let player = currentAVPlayer else { return }
+ if let currentItem = player.currentItem {
+ if currentItem.status == .readyToPlay {
player.play()
- playerStatus = .playing
- addTimeObserver()
- addEndOfItemObserver()
+ addTimeObservers()
+ } else {
+ autoplay = true
}
-
- func pause() {
- guard let player = currentAVPlayer else { return }
- player.pause()
- playerStatus = .paused
- removeTimeObserver()
- removeEndOfItemObserver()
}
-
- func addTimeObserver() {
- guard let player = currentAVPlayer else { return }
- timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1.0, Int32(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [weak self] _ in
- self?.delegate?.playerDidUpdateTime()
- })
}
- func removeTimeObserver() {
- guard let token = timeObserverToken, let player = currentAVPlayer else { return }
-
- player.removeTimeObserver(token)
- timeObserverToken = nil
+ func pause() {
+ if let currentItem = player.currentItem {
+ if currentItem.status == .readyToPlay {
+ player.pause()
+ removeTimeObservers()
+ } else {
+ autoplay = false
}
-
- func addEndOfItemObserver() {
- NotificationCenter.default.addObserver(self, selector: #selector(currentItemDidFinish), name: .AVPlayerItemDidPlayToEndTime, object: currentAVPlayer?.currentItem)
}
-
- func removeEndOfItemObserver() {
- NotificationCenter.default.removeObserver(self)
}
func togglePlaying() {
- switch playerStatus {
- case .loading:
- shouldPlayWhenLoaded = !shouldPlayWhenLoaded
- case .playing:
+ if isPlaying {
pause()
- case .paused:
- play()
- case .finished:
- setProgress(progress: 0.0)
+ } else {
play()
- default:
- break
}
+ delegate?.updateUI()
}
- func skip(seconds: Float64) {
- guard let player = currentAVPlayer, playerStatus != .empty || playerStatus != .loading else { return }
- let newTime = CMTimeAdd(player.currentTime(), CMTimeMakeWithSeconds(seconds, Int32(NSEC_PER_SEC)))
-
- // if player had finished and skip is backwards in time, move to a paused state
- if playerStatus == .finished && seconds < 0 {
- playerStatus = .paused
+ func skip(seconds: Double) {
+ if let currentTime = player.currentItem?.currentTime() {
+ let newTime = CMTimeAdd(currentTime, CMTime(seconds: seconds, preferredTimescale: CMTimeScale(1.0)))
+ player.currentItem?.seek(to: newTime)
+ delegate?.updateUI()
}
- player.seek(to: newTime)
- delegate?.playerDidUpdateTime()
}
-
- func getProgress() -> Float {
- if playerStatus == .empty || playerStatus == .loading || currentAVPlayer?.currentItem == nil {
- return 0.0
+ func getProgress() -> Double {
+ if let currentItem = player.currentItem {
+ let currentTime = currentItem.currentTime()
+ let durationTime = currentItem.duration
+ if !durationTime.isIndefinite && durationTime.seconds != 0 {
+ return currentTime.seconds / durationTime.seconds
}
- return Float((currentAVPlayer?.currentItem?.currentTime().seconds)!) / Float((currentAVPlayer?.currentItem?.duration.seconds)!)
+ }
+ return 0.0
}
- func setProgress(progress: Float) {
- if playerStatus == .empty || playerStatus == .loading {
- return
+ func setProgress(progress: Double) {
+ if let duration = player.currentItem?.duration {
+ if !duration.isIndefinite {
+ player.currentItem!.seek(to: CMTime(seconds: duration.seconds * min(max(progress, 0.0), 1.0), preferredTimescale: CMTimeScale(1.0)))
+ delegate?.updateUI()
+ }
+ }
}
- var progress = progress
+ // Warning: these next three functions should only be used to set UI element values
- if progress < 0.0 {
- progress = 0.0
- } else if progress > 1.0 {
- progress = 1.0
+ func currentItemDuration() -> CMTime {
+ guard let duration = player.currentItem?.duration else {
+ return CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(1.0))
+ }
+ return duration.isIndefinite ? CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(1.0)) : duration
}
- if let duration = currentAVPlayer?.currentItem?.duration {
- let newTime = progress * Float(duration.seconds)
- currentAVPlayer?.seek(to: CMTimeMakeWithSeconds(Float64(newTime), Int32(NSEC_PER_SEC)))
- if playerStatus == .finished {
- playerStatus = .paused
+ func currentItemElapsedTime() -> CMTime {
+ return player.currentItem?.currentTime() ?? CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(1.0))
}
+
+ func currentItemRemainingTime() -> CMTime {
+ guard let duration = player.currentItem?.duration else {
+ return CMTime(seconds: 0.0, preferredTimescale: CMTimeScale(1.0))
}
+ let elapsedTime = currentItemElapsedTime()
+ return CMTimeSubtract(duration, elapsedTime)
}
- func getDuration() -> CMTime? {
- return currentAVPlayer?.currentItem?.duration
- }
+ // Mark: KVO methods
- func getTimePassed() -> CMTime? {
- if playerStatus == .finished {
- return getDuration()
+ func addTimeObservers() {
+ timeObserverToken = player.addPeriodicTimeObserver(forInterval: CMTimeMakeWithSeconds(1.0, Int32(NSEC_PER_SEC)), queue: DispatchQueue.main, using: { [weak self] _ in
+ self?.delegate?.updateUI()
+ })
+ NotificationCenter.default.addObserver(self, selector: #selector(currentItemDidPlayToEndTime), name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}
- return currentAVPlayer?.currentItem?.currentTime()
+
+ func removeTimeObservers() {
+ guard let token = timeObserverToken else { return }
+ player.removeTimeObserver(token)
+ timeObserverToken = nil
+ NotificationCenter.default.removeObserver(self, name: .AVPlayerItemDidPlayToEndTime, object: player.currentItem)
}
- func getTimeLeft() -> CMTime? {
- guard let timePassed = getTimePassed(), let duration = getDuration() else {
- return nil
+ func removeCurrentItemStatusObserver() {
+ // observeValue(...) will take care of removing AVPlayerItem.status observer once it is
+ // readyToPlay, so we only need to remove observer if AVPlayerItem isn't readyToPlay yet
+ if let currentItem = player.currentItem {
+ if currentItem.status != .readyToPlay {
+ currentItem.removeObserver(self,
+ forKeyPath: #keyPath(AVPlayer.status),
+ context: &playerItemContext)
}
- return CMTimeSubtract(duration, timePassed)
}
-
- func currentItemDidFinish() {
- playerStatus = .finished
- removeEndOfItemObserver()
- delegate?.playerDidChangeState()
- delegate?.playerDidUpdateTime()
}
- func shouldDisplayPlayButton() -> Bool {
- switch playerStatus {
- case .empty, .finished, .paused:
- // equivalend to .paused; should indicate that Player instance has paused playback
- return true
- case .playing:
- return false
- case .loading:
- // equivalent to .playing; in this case, playback is defined
- // by whether Player will autoplay once track is loaded
- return !shouldPlayWhenLoaded
- }
+ func currentItemDidPlayToEndTime() {
+ removeTimeObservers()
+ delegate?.updateUI()
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
- guard context == &playerContext else {
- super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
+ // Only handle observations for the playerItemContext
+ guard context == &playerItemContext else {
+ super.observeValue(forKeyPath: keyPath,
+ of: object,
+ change: change,
+ context: context)
return
}
- if keyPath == #keyPath(AVPlayer.status) {
- let status: AVPlayerStatus
+ if keyPath == #keyPath(AVPlayerItem.status) {
+ let status: AVPlayerItemStatus
if let statusNumber = change?[.newKey] as? NSNumber {
- status = AVPlayerStatus(rawValue: statusNumber.intValue)!
+ status = AVPlayerItemStatus(rawValue: statusNumber.intValue)!
} else {
status = .unknown
}
switch status {
case .readyToPlay:
- playerStatus = .paused
- if shouldPlayWhenLoaded {
- togglePlaying()
- }
+ print("AVPlayerItem ready to play")
+ currentItemPrepared = true
+ if autoplay { play() }
case .failed:
- playerStatus = .empty
- default:
- break
- }
- currentAVPlayer?.removeObserver(self, forKeyPath: #keyPath(AVPlayer.status), context: &playerContext)
+ print("Failed to load AVPlayerItem")
+ case .unknown:
+ print("Unknown AVPlayerItemStatus")
+ }
+ // remove observer after having reading the AVPlayerItem status
+ player.currentItem?.removeObserver(self,
+ forKeyPath: #keyPath(AVPlayerItem.status),
+ context: &playerItemContext)
}
}
}

diff:
@@ -41,7 +41,7 @@ class PlayerViewController: UIViewController, PlayerDelegate {
var backwardsLabel: UILabel!
var rightTimeLabel: UILabel!
var leftTimeLabel: UILabel!
- var isScrubbing = false
+ var scrubbing = false
override func viewDidLoad() {
super.viewDidLoad()
@@ -60,17 +60,11 @@ class PlayerViewController: UIViewController, PlayerDelegate {
episodeNameLabel = UILabel(frame: .zero)
episodeNameLabel.textAlignment = .center
episodeNameLabel.font = .boldSystemFont(ofSize: 15)
- episodeNameLabel.text = "Episode Name"
- episodeNameLabel.sizeToFit()
- episodeNameLabel.center = CGPoint(x: view.frame.width/2, y: EpisodeNameLabelYValue)
controlsView.addSubview(episodeNameLabel)
seriesNameLabel = UILabel(frame: .zero)
seriesNameLabel.textAlignment = .center
seriesNameLabel.font = .systemFont(ofSize: 13)
- seriesNameLabel.text = "Series Name"
- seriesNameLabel.sizeToFit()
- seriesNameLabel.center = CGPoint(x: view.frame.width/2, y: SeriesNameLabelYValue)
controlsView.addSubview(seriesNameLabel)
slider = UISlider(frame: .zero)
@@ -87,17 +81,11 @@ class PlayerViewController: UIViewController, PlayerDelegate {
leftTimeLabel = UILabel(frame: .zero)
leftTimeLabel.font = .systemFont(ofSize: 12)
leftTimeLabel.textAlignment = .center
- leftTimeLabel.text = "-:--"
- leftTimeLabel.sizeToFit()
- leftTimeLabel.center = CGPoint(x: TimeLabelHorizontalInset + leftTimeLabel.frame.width/2, y: SliderCenterY)
controlsView.addSubview(leftTimeLabel)
rightTimeLabel = UILabel(frame: .zero)
rightTimeLabel.font = .systemFont(ofSize: 12)
rightTimeLabel.textAlignment = .center
- rightTimeLabel.text = "-:--"
- rightTimeLabel.sizeToFit()
- rightTimeLabel.center = CGPoint(x: view.frame.width - rightTimeLabel.frame.width/2 - TimeLabelHorizontalInset, y: SliderCenterY)
controlsView.addSubview(rightTimeLabel)
playPauseButton = UIButton(frame: .zero)
@@ -140,32 +128,20 @@ class PlayerViewController: UIViewController, PlayerDelegate {
}
override func viewDidAppear(_ animated: Bool) {
- playerDidUpdateTime()
- playerDidChangeState()
+ updateUI()
}
func endScrubbing() {
- Player.sharedInstance.setProgress(progress: slider.value)
- isScrubbing = false
+ Player.sharedInstance.setProgress(progress: Double(slider.value))
+ scrubbing = false
+ updateUI()
}
func sliderValueChanged() {
- isScrubbing = true
-
- // update time labels to be the slider value
- guard let duration = Player.sharedInstance.getDuration() else { return }
-
- let newSeconds = duration.seconds * Double(slider.value)
- let newTimePassed = CMTime(seconds: newSeconds, preferredTimescale: Int32(NSEC_PER_SEC))
- let newTimeLeft = CMTimeSubtract(duration, newTimePassed)
-
- leftTimeLabel.text = newTimePassed.durationText
- leftTimeLabel.sizeToFit()
- rightTimeLabel.text = newTimeLeft.durationText
- rightTimeLabel.sizeToFit()
+ scrubbing = true
+ // TODO: update time labels as you scrub
}
-
func playPauseButtonPress() {
Player.sharedInstance.togglePlaying()
}
@@ -178,35 +154,39 @@ class PlayerViewController: UIViewController, PlayerDelegate {
Player.sharedInstance.skip(seconds: -15.0)
}
- // Mark: PlayerDelegate
-
- func playerDidUpdateTime() {
- // don't update slider value and time labels if currently scrubbing
- if isScrubbing { return }
+ // Mark: PlayerDelegate protocol
- slider.value = Player.sharedInstance.getProgress()
-
- if let timePassed = Player.sharedInstance.getTimePassed() {
- leftTimeLabel.text = timePassed.durationText
+ func updateUI() {
+ if Player.sharedInstance.isPlaying {
+ playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Pause"), for: .normal)
} else {
- leftTimeLabel.text = "-:--"
+ playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Play"), for: .normal)
}
- if let timeLeft = Player.sharedInstance.getTimeLeft() {
- rightTimeLabel.text = timeLeft.durationText
+ if let episode = Player.sharedInstance.currentEpisode {
+ leftTimeLabel.text = Player.sharedInstance.currentItemElapsedTime().descriptionText
+ rightTimeLabel.text = Player.sharedInstance.currentItemRemainingTime().descriptionText
+ episodeNameLabel.text = episode.title
+ seriesNameLabel.text = episode.series?.title ?? "no series"
} else {
- rightTimeLabel.text = "-:--"
+ leftTimeLabel.text = "0:00"
+ rightTimeLabel.text = "0:00"
+ episodeNameLabel.text = "no episode"
+ seriesNameLabel.text = "no series"
}
leftTimeLabel.sizeToFit()
rightTimeLabel.sizeToFit()
- }
+ episodeNameLabel.sizeToFit()
+ seriesNameLabel.sizeToFit()
- func playerDidChangeState() {
- if Player.sharedInstance.shouldDisplayPlayButton() {
- playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Play"), for: .normal)
- } else {
- playPauseButton.setBackgroundImage(#imageLiteral(resourceName: "Pause"), for: .normal)
+ leftTimeLabel.center = CGPoint(x: TimeLabelHorizontalInset + leftTimeLabel.frame.width/2, y: SliderCenterY)
+ rightTimeLabel.center = CGPoint(x: view.frame.width - rightTimeLabel.frame.width/2 - TimeLabelHorizontalInset, y: SliderCenterY)
+ episodeNameLabel.center = CGPoint(x: view.frame.width/2, y: EpisodeNameLabelYValue)
+ seriesNameLabel.center = CGPoint(x: view.frame.width/2, y: SeriesNameLabelYValue)
+
+ if !scrubbing {
+ slider.value = Float(Player.sharedInstance.getProgress())
}
}
}

original_message: big improvements to player


message: create views for the new player

diff:
/* Begin PBXBuildFile section */
421501271D9F5FE1002839D1 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421501261D9F5FE1002839D1 /* Player.swift */; };
+ 424EAA391E5DDF9900F0126C /* TempPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424EAA381E5DDF9900F0126C /* TempPlayerViewController.swift */; };
+ 425B393D1E512F5D00283CA4 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425B393C1E512F5D00283CA4 /* PlayerControlsView.swift */; };
+ 425C90621E5A5827004BB7AD /* EpisodeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425C90611E5A5827004BB7AD /* EpisodeDetailView.swift */; };
+ 4290028A1E5CA6A20069F0BF /* PlayerHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429002891E5CA6A20069F0BF /* PlayerHeaderView.swift */; };
42D7D98D1D80C557000691A4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D7D98C1D80C557000691A4 /* AppDelegate.swift */; };
42D7D9941D80C557000691A4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42D7D9931D80C557000691A4 /* Assets.xcassets */; };
42D7D9971D80C557000691A4 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 42D7D9951D80C557000691A4 /* LaunchScreen.storyboard */; };
/* Begin PBXFileReference section */
13C8D9A7A7FB0C3D27C81500 /* Pods-PodcastTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PodcastTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PodcastTests/Pods-PodcastTests.debug.xcconfig"; sourceTree = "<group>"; };
421501261D9F5FE1002839D1 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
+ 424EAA381E5DDF9900F0126C /* TempPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TempPlayerViewController.swift; path = ../TempPlayerViewController.swift; sourceTree = "<group>"; };
+ 425B393C1E512F5D00283CA4 /* PlayerControlsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
+ 425C90611E5A5827004BB7AD /* EpisodeDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeDetailView.swift; sourceTree = "<group>"; };
+ 429002891E5CA6A20069F0BF /* PlayerHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerHeaderView.swift; sourceTree = "<group>"; };
42D7D9891D80C557000691A4 /* Podcast.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Podcast.app; sourceTree = BUILT_PRODUCTS_DIR; };
42D7D98C1D80C557000691A4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
42D7D9931D80C557000691A4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
+ 425B393B1E512E8000283CA4 /* Player */ = {
+ isa = PBXGroup;
+ children = (
+ 425B393C1E512F5D00283CA4 /* PlayerControlsView.swift */,
+ 425C90611E5A5827004BB7AD /* EpisodeDetailView.swift */,
+ 429002891E5CA6A20069F0BF /* PlayerHeaderView.swift */,
+ );
+ name = Player;
+ sourceTree = "<group>";
+ };
42D7D9801D80C557000691A4 = {
isa = PBXGroup;
children = (
D3DB5A611DA1879F00E550E8 /* Utilities */,
D393D7531D8F0CC600241460 /* Cells */,
956D851D1E5194AF00D95F7A /* Misc */,
+ 42D7D98C1D80C557000691A4 /* AppDelegate.swift */,
+ FD9421401DB5306200685E14 /* Views */,
+ D39BEE191D8A04C900131936 /* Models */,
+ 7F97FBDD1D8663000073FC0F /* REST.swift */,
+ 42F684EA1D80CC7E0035217A /* PlayerViewController.swift */,
+ 421501261D9F5FE1002839D1 /* Player.swift */,
+ 424EAA381E5DDF9900F0126C /* TempPlayerViewController.swift */,
+ FDF1D1CE1DAEE2E300697379 /* ProfileViewController.swift */,
+ 42D7D9901D80C557000691A4 /* Main.storyboard */,
+ 42D7D9931D80C557000691A4 /* Assets.xcassets */,
+ 42D7D9951D80C557000691A4 /* LaunchScreen.storyboard */,
+ 42D7D9981D80C557000691A4 /* Info.plist */,
+ 42F684EC1D80CC9F0035217A /* DiscoverViewController.swift */,
+ 7F9D0E8F1D85029C00D2BC1C /* Podcast-Bridging-Header.h */,
+ 7F9D0E961D85A37700D2BC1C /* LoginViewController.swift */,
+ D300C0911DAEF801000CDBF5 /* CategoriesFeedViewController.swift */,
+ D37D799F1DEF920000D752F0 /* SearchViewController.swift */,
);
path = Podcast;
sourceTree = "<group>";
isa = PBXGroup;
children = (
FD9421411DB5307B00685E14 /* ProfileHeaderView.swift */,
+ 425B393B1E512E8000283CA4 /* Player */,
FD0159FF1DCBFC80003E7B4E /* ProfileMiniHeader.swift */,
FDF5E0CE1DD3A091005CCB53 /* FollowButton.swift */,
FD8F7DFD1DDD36D600DCC511 /* ProfileSectionHeaderView.swift */,
956D851A1E5193F000D95F7A /* UIFont+.swift in Sources */,
FD295FDB1E6258B500520526 /* EpisodeTableViewCell.swift in Sources */,
FD20F5741E52186500721C46 /* SeriesDetailHeaderView.swift in Sources */,
+ 425B393D1E512F5D00283CA4 /* PlayerControlsView.swift in Sources */,
FDF5E0CF1DD3A091005CCB53 /* FollowButton.swift in Sources */,
7F97FBDA1D85A99F0073FC0F /* Constants.swift in Sources */,
447DA8621E5C99790002BCA0 /* RecommendedTagsCollectionViewCell.swift in Sources */,
447DA8631E5C99790002BCA0 /* RecommendedTagsTableViewCell.swift in Sources */,
7F9D0E971D85A37700D2BC1C /* LoginViewController.swift in Sources */,
FD8F7DFC1DDD20FC00DCC511 /* SeriesCollectionViewCell.swift in Sources */,
+ 425C90621E5A5827004BB7AD /* EpisodeDetailView.swift in Sources */,
FD015A001DCBFC80003E7B4E /* ProfileMiniHeader.swift in Sources */,
42F684ED1D80CC9F0035217A /* FeedViewController.swift in Sources */,
952BD1181E66A2AE006C0370 /* BookmarkViewController.swift in Sources */,
D300C0921DAEF801000CDBF5 /* CategoriesFeedViewController.swift in Sources */,
D3DB5A651DA18FC600E550E8 /* CategoriesCollectionViewCell.swift in Sources */,
FD20F5721E52067400721C46 /* SeriesDetailViewController.swift in Sources */,
+ 4290028A1E5CA6A20069F0BF /* PlayerHeaderView.swift in Sources */,
FD9421421DB5307B00685E14 /* ProfileHeaderView.swift in Sources */,
956D85181E5193C200D95F7A /* UIColor+.swift in Sources */,
D37D79A01DEF920000D752F0 /* SearchViewController.swift in Sources */,
447DA8691E5C998E0002BCA0 /* DiscoverTableViewHeaderView.swift in Sources */,
D3E2692F1E5E806A00793569 /* TagCard.swift in Sources */,
956D85131E51926800D95F7A /* EndpointRequest.swift in Sources */,
+ 424EAA391E5DDF9900F0126C /* TempPlayerViewController.swift in Sources */,
42D7D98D1D80C557000691A4 /* AppDelegate.swift in Sources */,
D39BEE1D1D8A073200131936 /* Series.swift in Sources */,
42F684EB1D80CC7E0035217A /* PlayerViewController.swift in Sources */,

diff:
"images" : [
{
"idiom" : "universal",
- "filename" : "mock-podcast-artwork.jpg",
+ "filename" : "icon_344098539-f089cf02094b52708d0979b026a157aa5a1e3ac1.jpg",
"scale" : "1x"
},
{
"idiom" : "universal",
- "filename" : "mock-podcast-artwork-1.jpg",
+ "filename" : "icon_344098539-f089cf02094b52708d0979b026a157aa5a1e3ac1-1.jpg",
"scale" : "2x"
},
{
"idiom" : "universal",
- "filename" : "mock-podcast-artwork-2.jpg",
+ "filename" : "icon_344098539-f089cf02094b52708d0979b026a157aa5a1e3ac1-2.jpg",
"scale" : "3x"
}
],

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/sample_series_artwork.imageset/icon_344098539-f089cf02094b52708d0979b026a157aa5a1e3ac1-1.jpg differ

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/sample_series_artwork.imageset/icon_344098539-f089cf02094b52708d0979b026a157aa5a1e3ac1-2.jpg differ

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/sample_series_artwork.imageset/icon_344098539-f089cf02094b52708d0979b026a157aa5a1e3ac1.jpg differ

diff: Binary files a/Podcast/Assets.xcassets/sample_series_artwork.imageset/mock-podcast-artwork-1.jpg and /dev/null differ

diff: Binary files a/Podcast/Assets.xcassets/sample_series_artwork.imageset/mock-podcast-artwork-2.jpg and /dev/null differ

diff: Binary files a/Podcast/Assets.xcassets/sample_series_artwork.imageset/mock-podcast-artwork.jpg and /dev/null differ

diff:
@@ -19,6 +19,7 @@ class Player: NSObject {
player = AVPlayer()
autoplay = true
currentItemPrepared = false
+ scrubbing = false
super.init()
}
@@ -41,10 +42,12 @@ class Player: NSObject {
didSet {
autoplay = true
currentItemPrepared = false
+ scrubbing = false
}
}
private var autoplay: Bool
private var currentItemPrepared: Bool
+ var scrubbing: Bool
var isPlaying: Bool {
get {
return player.rate != 0.0 || (!currentItemPrepared && autoplay && (player.currentItem != nil))

diff:
@@ -11,118 +11,35 @@ import CoreMedia
class PlayerViewController: UIViewController, PlayerDelegate {
- // Mark: Constants
-
- let PlayerControlPanelHeight: CGFloat = 182
-
- let ForwardsBackwardsButtonSize: CGFloat = 44
- let PlayPauseButtonSize: CGFloat = 40
- let SliderHeight: CGFloat = 6
- let SliderInset: CGFloat = 48
- let SliderCenterY: CGFloat = 78
- let EpisodeNameLabelYValue: CGFloat = 21
- let SeriesNameLabelYValue: CGFloat = 40
- let TimeLabelHorizontalInset: CGFloat = 14
- let ControlButtonsCenterY: CGFloat = 120
- let ControlButtonsCenterHorizontalOffset: CGFloat = 55
-
- // Mark: Properties
-
- var controlsView: UIView!
- var artworkImageView: UIImageView!
-
+ var controlsView: PlayerControlsView!
+ var episodeDetailView: EpisodeDetailView!
+ var playerHeaderView: PlayerHeaderView!
var episodeNameLabel: UILabel!
var seriesNameLabel: UILabel!
- var slider: UISlider!
- var playPauseButton: UIButton!
- var forwardsButton: UIButton!
- var forwardsLabel: UILabel!
- var backwardsButton: UIButton!
- var backwardsLabel: UILabel!
- var rightTimeLabel: UILabel!
- var leftTimeLabel: UILabel!
- var scrubbing = false
+
+ let EpisodeDetailViewYVal: CGFloat = 87
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .podcastGrayLight
- tabBarController?.title = "Now Playing"
-
- artworkImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: view.frame.height - PlayerControlPanelHeight - tabBarController!.tabBar.frame.height))
- artworkImageView.contentMode = .scaleAspectFill
- artworkImageView.image = UIImage(named: "sample_series_artwork")
- view.addSubview(artworkImageView)
-
- controlsView = UIView(frame: CGRect(x: 0, y: view.frame.height - PlayerControlPanelHeight - tabBarController!.tabBar.frame.height, width: view.frame.width, height: PlayerControlPanelHeight))
- controlsView.backgroundColor = .white
- view.addSubview(controlsView)
- episodeNameLabel = UILabel(frame: .zero)
- episodeNameLabel.textAlignment = .center
- episodeNameLabel.font = .boldSystemFont(ofSize: 15)
- controlsView.addSubview(episodeNameLabel)
+ playerHeaderView = PlayerHeaderView(frame: .zero)
+ playerHeaderView.frame.size.width = view.frame.width
+ playerHeaderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(playerHeaderTapped)))
+ view.addSubview(playerHeaderView)
- seriesNameLabel = UILabel(frame: .zero)
- seriesNameLabel.textAlignment = .center
- seriesNameLabel.font = .systemFont(ofSize: 13)
- controlsView.addSubview(seriesNameLabel)
+ episodeDetailView = EpisodeDetailView(frame: .zero)
+ episodeDetailView.frame.size.width = view.frame.width
+ episodeDetailView.frame.origin.y = EpisodeDetailViewYVal
+ view.addSubview(episodeDetailView)
- slider = UISlider(frame: .zero)
- slider.thumbTintColor = .podcastGreenBlue
- slider.minimumTrackTintColor = .podcastBlueLight
- slider.maximumTrackTintColor = .podcastBlueLight
- slider.addTarget(self, action: #selector(sliderValueChanged), for: .valueChanged)
- slider.addTarget(self, action: #selector(endScrubbing), for: .touchUpInside)
- slider.addTarget(self, action: #selector(endScrubbing), for: .touchUpOutside)
- slider.frame = CGRect(x: 0, y: 0, width: view.frame.width - (2 * SliderInset), height: SliderHeight)
- slider.center = CGPoint(x: view.frame.width/2, y: SliderCenterY)
- controlsView.addSubview(slider)
-
- leftTimeLabel = UILabel(frame: .zero)
- leftTimeLabel.font = .systemFont(ofSize: 12)
- leftTimeLabel.textAlignment = .center
- controlsView.addSubview(leftTimeLabel)
-
- rightTimeLabel = UILabel(frame: .zero)
- rightTimeLabel.font = .systemFont(ofSize: 12)
- rightTimeLabel.textAlignment = .center
- controlsView.addSubview(rightTimeLabel)
-
- playPauseButton = UIButton(frame: .zero)
- playPauseButton.setBackgroundImage(UIImage(named: "play_icon"), for: .normal)
- playPauseButton.adjustsImageWhenHighlighted = false
- playPauseButton.addTarget(self, action: #selector(playPauseButtonPress), for: .touchUpInside)
- playPauseButton.frame = CGRect(x: 0, y: 0, width: PlayPauseButtonSize, height: PlayPauseButtonSize)
- playPauseButton.center = CGPoint(x: view.frame.width/2, y: ControlButtonsCenterY)
- controlsView.addSubview(playPauseButton)
-
- forwardsButton = UIButton(frame: .zero)
- forwardsButton.setBackgroundImage(UIImage(named: "skip_forward_icon"), for: .normal)
- forwardsButton.adjustsImageWhenHighlighted = false
- forwardsButton.addTarget(self, action: #selector(forwardButtonPress), for: .touchUpInside)
- forwardsButton.frame = CGRect(x: 0, y: 0, width: ForwardsBackwardsButtonSize, height: ForwardsBackwardsButtonSize)
- forwardsButton.center = CGPoint(x: playPauseButton.frame.maxX + ControlButtonsCenterHorizontalOffset, y: ControlButtonsCenterY)
- controlsView.addSubview(forwardsButton)
-
- forwardsLabel = UILabel(frame: .zero)
- forwardsLabel.text = "30"
- forwardsLabel.sizeToFit()
- forwardsLabel.center = forwardsButton.center
- controlsView.addSubview(forwardsLabel)
+ controlsView = PlayerControlsView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 0))
+ controlsView.frame.origin.y = view.frame.height - controlsView.frame.size.height
+ view.addSubview(controlsView)
- backwardsButton = UIButton(frame: .zero)
- backwardsButton.setBackgroundImage(UIImage(named: "skip_backward_icon"), for: .normal)
- backwardsButton.adjustsImageWhenHighlighted = false
- backwardsButton.addTarget(self, action: #selector(backwardButtonPress), for: .touchUpInside)
- backwardsButton.frame = CGRect(x: 0, y: 0, width: ForwardsBackwardsButtonSize, height: ForwardsBackwardsButtonSize)
- backwardsButton.center = CGPoint(x: playPauseButton.frame.minX - ControlButtonsCenterHorizontalOffset, y: ControlButtonsCenterY)
- controlsView.addSubview(backwardsButton)
+ // TODO: add comments section
- backwardsLabel = UILabel(frame: .zero)
- backwardsLabel.text = "15"
- backwardsLabel.sizeToFit()
- backwardsLabel.center = backwardsButton.center
- controlsView.addSubview(backwardsLabel)
+ updateUI()
Player.sharedInstance.delegate = self
}
@@ -131,62 +48,13 @@ class PlayerViewController: UIViewController, PlayerDelegate {
updateUI()
}
- func endScrubbing() {
- Player.sharedInstance.setProgress(progress: Double(slider.value))
- scrubbing = false
- updateUI()
+ func playerHeaderTapped() {
+ dismiss(animated: true, completion: nil)
}
- func sliderValueChanged() {
- scrubbing = true
- // TODO: update time labels as you scrub
- }
-
- func playPauseButtonPress() {
- Player.sharedInstance.togglePlaying()
- }
-
- func forwardButtonPress() {
- Player.sharedInstance.skip(seconds: 30.0)
- }
-
- func backwardButtonPress() {
- Player.sharedInstance.skip(seconds: -15.0)
- }
-
- // Mark: PlayerDelegate protocol
-
func updateUI() {
- if Player.sharedInstance.isPlaying {
- playPauseButton.setBackgroundImage(UIImage(named: "pause_icon"), for: .normal)
- } else {
- playPauseButton.setBackgroundImage(UIImage(named: "play_icon"), for: .normal)
- }
-
- if let episode = Player.sharedInstance.currentEpisode {
- leftTimeLabel.text = Player.sharedInstance.currentItemElapsedTime().descriptionText
- rightTimeLabel.text = Player.sharedInstance.currentItemRemainingTime().descriptionText
- episodeNameLabel.text = episode.title
- seriesNameLabel.text = episode.series?.title ?? "no series"
- } else {
- leftTimeLabel.text = "0:00"
- rightTimeLabel.text = "0:00"
- episodeNameLabel.text = "no episode"
- seriesNameLabel.text = "no series"
- }
-
- leftTimeLabel.sizeToFit()
- rightTimeLabel.sizeToFit()
- episodeNameLabel.sizeToFit()
- seriesNameLabel.sizeToFit()
-
- leftTimeLabel.center = CGPoint(x: TimeLabelHorizontalInset + leftTimeLabel.frame.width/2, y: SliderCenterY)
- rightTimeLabel.center = CGPoint(x: view.frame.width - rightTimeLabel.frame.width/2 - TimeLabelHorizontalInset, y: SliderCenterY)
- episodeNameLabel.center = CGPoint(x: view.frame.width/2, y: EpisodeNameLabelYValue)
- seriesNameLabel.center = CGPoint(x: view.frame.width/2, y: SeriesNameLabelYValue)
-
- if !scrubbing {
- slider.value = Float(Player.sharedInstance.getProgress())
- }
+ playerHeaderView.updateUI()
+ episodeDetailView.updateUI()
+ controlsView.updateUI()
}
}

original_message: create views for the new player


message: setup for mini player to episode detail transformation

diff:
/* Begin PBXBuildFile section */
421501271D9F5FE1002839D1 /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 421501261D9F5FE1002839D1 /* Player.swift */; };
- 424EAA391E5DDF9900F0126C /* TempPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424EAA381E5DDF9900F0126C /* TempPlayerViewController.swift */; };
+ 4253114D1E5E4CC500DF06F6 /* MiniPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4253114C1E5E4CC500DF06F6 /* MiniPlayerView.swift */; };
425B393D1E512F5D00283CA4 /* PlayerControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425B393C1E512F5D00283CA4 /* PlayerControlsView.swift */; };
425C90621E5A5827004BB7AD /* EpisodeDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 425C90611E5A5827004BB7AD /* EpisodeDetailView.swift */; };
4290028A1E5CA6A20069F0BF /* PlayerHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429002891E5CA6A20069F0BF /* PlayerHeaderView.swift */; };
/* Begin PBXFileReference section */
13C8D9A7A7FB0C3D27C81500 /* Pods-PodcastTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PodcastTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PodcastTests/Pods-PodcastTests.debug.xcconfig"; sourceTree = "<group>"; };
421501261D9F5FE1002839D1 /* Player.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
- 424EAA381E5DDF9900F0126C /* TempPlayerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TempPlayerViewController.swift; path = ../TempPlayerViewController.swift; sourceTree = "<group>"; };
+ 4253114C1E5E4CC500DF06F6 /* MiniPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MiniPlayerView.swift; sourceTree = "<group>"; };
425B393C1E512F5D00283CA4 /* PlayerControlsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerControlsView.swift; sourceTree = "<group>"; };
425C90611E5A5827004BB7AD /* EpisodeDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeDetailView.swift; sourceTree = "<group>"; };
429002891E5CA6A20069F0BF /* PlayerHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerHeaderView.swift; sourceTree = "<group>"; };
425B393C1E512F5D00283CA4 /* PlayerControlsView.swift */,
425C90611E5A5827004BB7AD /* EpisodeDetailView.swift */,
429002891E5CA6A20069F0BF /* PlayerHeaderView.swift */,
+ 4253114C1E5E4CC500DF06F6 /* MiniPlayerView.swift */,
);
name = Player;
sourceTree = "<group>";
7F97FBDD1D8663000073FC0F /* REST.swift */,
42F684EA1D80CC7E0035217A /* PlayerViewController.swift */,
421501261D9F5FE1002839D1 /* Player.swift */,
- 424EAA381E5DDF9900F0126C /* TempPlayerViewController.swift */,
FDF1D1CE1DAEE2E300697379 /* ProfileViewController.swift */,
42D7D9901D80C557000691A4 /* Main.storyboard */,
42D7D9931D80C557000691A4 /* Assets.xcassets */,
D3E2692B1E5E7CBF00793569 /* RecommendedCard.swift in Sources */,
D34A2EE51E6364830047AF32 /* Tag.swift in Sources */,
D300C0921DAEF801000CDBF5 /* CategoriesFeedViewController.swift in Sources */,
+ 4253114D1E5E4CC500DF06F6 /* MiniPlayerView.swift in Sources */,
D3DB5A651DA18FC600E550E8 /* CategoriesCollectionViewCell.swift in Sources */,
FD20F5721E52067400721C46 /* SeriesDetailViewController.swift in Sources */,
4290028A1E5CA6A20069F0BF /* PlayerHeaderView.swift in Sources */,
447DA8691E5C998E0002BCA0 /* DiscoverTableViewHeaderView.swift in Sources */,
D3E2692F1E5E806A00793569 /* TagCard.swift in Sources */,
956D85131E51926800D95F7A /* EndpointRequest.swift in Sources */,
- 424EAA391E5DDF9900F0126C /* TempPlayerViewController.swift in Sources */,
42D7D98D1D80C557000691A4 /* AppDelegate.swift in Sources */,
D39BEE1D1D8A073200131936 /* Series.swift in Sources */,
42F684EB1D80CC7E0035217A /* PlayerViewController.swift in Sources */,

diff:
@@ -21,6 +21,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var profileViewController: ProfileViewController!
var bookmarkViewController: BookmarkViewController!
var feedViewControllerNavigationController: UINavigationController!
+ var playerViewController: PlayerViewController!
var discoverViewControllerNavigationController: UINavigationController!
var profileViewControllerNavigationController: UINavigationController!
var bookmarkViewControllerNavigationController: UINavigationController!
@@ -31,6 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
feedViewController = FeedViewController()
profileViewController = ProfileViewController()
bookmarkViewController = BookmarkViewController()
+ playerViewController = PlayerViewController()
discoverViewControllerNavigationController = UINavigationController(rootViewController: discoverViewController)
feedViewControllerNavigationController = UINavigationController(rootViewController: feedViewController)
@@ -46,7 +48,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// Tab bar controller
tabBarController = TabBarController()
tabBarController.transparentTabBarEnabled = true
- tabBarController.numberOfTabs = 4
+ tabBarController.numberOfTabs = 5
tabBarController.setUnselectedImage(image: UIImage(named: "home_unselected_icon")!, forTabAtIndex: 0)
tabBarController.setUnselectedImage(image: UIImage(named: "magnifying_glass_unselected_icon")!, forTabAtIndex: 1)
tabBarController.setUnselectedImage(image: UIImage(named: "bookmark_unselected_icon")!, forTabAtIndex: 2)
@@ -72,6 +74,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
self.tabBarController.present(self.profileViewControllerNavigationController, animated: false, completion: nil)
}, forTabAtIndex: 3)
+ tabBarController.addBlockToExecuteOnTabBarButtonPress(block: {
+ self.tabBarController.present(self.playerViewController, animated: false, completion: nil)
+ }, forTabAtIndex: 4)
+
// Main window setup
window = UIWindow()

diff:
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "Line.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "Line@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "Line@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/down_arrow.imageset/Line.png differ

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/down_arrow.imageset/Line@2x.png differ

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/down_arrow.imageset/Line@3x.png differ

diff:
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "PlayButton.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "PlayButton@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "PlayButton@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/mini_play_icon.imageset/PlayButton.png differ

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/mini_play_icon.imageset/PlayButton@2x.png differ

diff: Binary files /dev/null and b/Podcast/Assets.xcassets/mini_play_icon.imageset/PlayButton@3x.png differ

diff:
@@ -36,7 +36,7 @@ class PlayerControlsView: UIView {
backgroundColor = .white
slider = UISlider(frame: .zero)
- slider.frame = CGRect(x: 0, y: 0, width: frame.width - (2 * SliderInset), height: SliderHeight)
+ slider.frame.size = CGSize(width: frame.width - (2 * SliderInset), height: SliderHeight)
slider.center = CGPoint(x: frame.width/2, y: SliderCenterY)
addSubview(slider)

diff:
@@ -14,31 +14,33 @@ class PlayerViewController: UIViewController, PlayerDelegate {
var controlsView: PlayerControlsView!
var episodeDetailView: EpisodeDetailView!
var playerHeaderView: PlayerHeaderView!
- var episodeNameLabel: UILabel!
- var seriesNameLabel: UILabel!
-
- let EpisodeDetailViewYVal: CGFloat = 87
+ var miniPlayerView: MiniPlayerView!
+ var isMini: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
- view.backgroundColor = .podcastGrayLight
+ view.backgroundColor = .white
playerHeaderView = PlayerHeaderView(frame: .zero)
playerHeaderView.frame.size.width = view.frame.width
- playerHeaderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(playerHeaderTapped)))
+ playerHeaderView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(changeMode)))
+ playerHeaderView.alpha = 0.0
view.addSubview(playerHeaderView)
+ miniPlayerView = MiniPlayerView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 0))
+ miniPlayerView.frame.size.width = view.frame.width
+ miniPlayerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(changeMode)))
+ view.addSubview(miniPlayerView)
+
episodeDetailView = EpisodeDetailView(frame: .zero)
episodeDetailView.frame.size.width = view.frame.width
- episodeDetailView.frame.origin.y = EpisodeDetailViewYVal
+ episodeDetailView.frame.origin.y = playerHeaderView.frame.maxY
view.addSubview(episodeDetailView)
controlsView = PlayerControlsView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 0))
controlsView.frame.origin.y = view.frame.height - controlsView.frame.size.height
view.addSubview(controlsView)
- // TODO: add comments section
-
updateUI()
Player.sharedInstance.delegate = self
@@ -48,13 +50,33 @@ class PlayerViewController: UIViewController, PlayerDelegate {
updateUI()
}
- func playerHeaderTapped() {
- dismiss(animated: true, completion: nil)
+ func changeMode() {
+ isMini ? expand() : collapse()
+ }
+
+ func expand() {
+ UIView.animate(withDuration: 1.0, animations: {
+ self.miniPlayerView.alpha = 0.0
+ self.playerHeaderView.alpha = 1.0
+ self.view.frame.origin.y = 0
+ })
+ isMini = false
+ }
+
+ func collapse() {
+ UIView.animate(withDuration: 1.0, animations: {
+ self.miniPlayerView.alpha = 1.0
+ self.playerHeaderView.alpha = 0.0
+ // TODO: is there a way to get the TabBar's height? Replace 110 with this value
+ self.view.frame.origin.y = self.view.frame.height - 110
+ })
+ isMini = true
}
func updateUI() {
playerHeaderView.updateUI()
episodeDetailView.updateUI()
controlsView.updateUI()
+ miniPlayerView.updateUI()
}
}


message: connect player to feed and bookmark tableview cells

diff:
@@ -90,7 +90,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
func showPlayer(animated: Bool) {
+ if tabBarController.accessoryViewController == playerViewController { return }
tabBarController.addAccessoryViewController(accessoryViewController: playerViewController)
+ collapsePlayer(animated: false)
}
func didFinishAuthenticatingUser() {

diff:
@@ -15,6 +15,7 @@ class BookmarkViewController: UIViewController, UITableViewDelegate, UITableView
///
var bookmarkTableView: UITableView!
var cards: [Card] = []
+ var currentlyPlayingIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
@@ -43,8 +44,16 @@ class BookmarkViewController: UIViewController, UITableViewDelegate, UITableView
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
- bookmarkTableView.reloadData()
+ // check before reloading data whether the Player has stopped playing the currentlyPlayingIndexPath
+ if let indexPath = currentlyPlayingIndexPath {
+ if let card = cards[indexPath.row] as? EpisodeCard {
+ if Player.sharedInstance.currentEpisode?.id != card.episode.id {
+ currentlyPlayingIndexPath = nil
+ }
+ }
+ }
+ bookmarkTableView.reloadData()
}
override func didReceiveMemoryWarning() {
@@ -66,6 +75,9 @@ class BookmarkViewController: UIViewController, UITableViewDelegate, UITableView
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CardTableViewCellIdentifier") as? CardTableViewCell else { return UITableViewCell() }
cell.delegate = self
cell.setupWithCard(card: cards[indexPath.row])
+ if indexPath == currentlyPlayingIndexPath {
+ cell.setPlayButtonToState(isPlaying: true)
+ }
return cell
}
@@ -100,8 +112,18 @@ class BookmarkViewController: UIViewController, UITableViewDelegate, UITableView
func cardTableViewCellDidPressPlayPauseButton(cardTableViewCell: CardTableViewCell) {
guard let cardIndexPath = bookmarkTableView.indexPath(for: cardTableViewCell), let card = cards[cardIndexPath.row] as? EpisodeCard else { return }
- //card.isPlaying = !card.isPlaying
- //cardTableViewCell.setPlayButtonToState(isPlaying: card.isPlaying)
+ if cardIndexPath == currentlyPlayingIndexPath { return }
+
+ if let indexPath = currentlyPlayingIndexPath {
+ if let cell = bookmarkTableView.cellForRow(at: indexPath) as? CardTableViewCell {
+ cell.setPlayButtonToState(isPlaying: false)
+ }
+ }
+ currentlyPlayingIndexPath = cardIndexPath
+ cardTableViewCell.setPlayButtonToState(isPlaying: true)
+ let appDelegate = UIApplication.shared.delegate as! AppDelegate
+ appDelegate.showPlayer(animated: true)
+ Player.sharedInstance.playEpisode(episode: card.episode)
}
func cardTableViewCellDidPressTagButton(cardTableViewCell: CardTableViewCell, index: Int) {

diff:
@@ -40,7 +40,7 @@ class Episode: NSObject {
self.smallArtworkImageURL = smallArtworkImageURL
self.largeArtworkImageURL = largeArtworkImageURL
if audioURL == nil { //TAKE THIS OUT LATER ONLY FOR PLAYER STATIC DATA
- self.audioURL = URL(string: "http://cf-media.sndcdn.com/stEcTqU4a27v.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vc3RFY1RxVTRhMjd2LjEyOC5tcDMiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE0ODg3NDQzMzJ9fX1dfQ__&Signature=SgM4MzYFr9CDFnX-4f-q6uWdawoVVtEqTCRf5AXPAlF83LXjXNzPVEv9PW2CVFaq-2qFZdabgXHv8Tjsz924k1mnfnu3qwpZjmez4MxnzajH47giPPfmd~F9Kv7WEP4vSLfUukrT4WMslxFFwNH~C7Hpwyrs8aUFU8naTEOH2LLIa6mE4IDa7eErsGkYZ5n-Zenata-kiTQHWYRVeJAzLRm69GX313YhUJQGGeYIE9TW2iYrteQkTmTr5JpJ2pwrbjDUfc1r3rkekpMLBuLBtt6qrB~IuL8nvm5kkDJENXABTBsoT7wHDxaAcWsnlAbL06Z5SIBWAst4rut5jZI0Cg__&Key-Pair-Id=APKAJAGZ7VMH2PFPW6UQ")
+ self.audioURL = URL(string: "https://play.podtrac.com/npr-344098539/npr.mc.tritondigital.com/WAITWAIT_PODCAST/media/anon.npr-podcasts/podcast/344098539/517256651/npr_517256651.mp3?orgId=1&d=2981&p=344098539&story=517256651&t=podcast&e=517256651&ft=pod&f=344098539")
} else {
self.audioURL = audioURL
}

diff:
@@ -288,12 +288,6 @@ class EpisodeTableViewCell: UITableViewCell {
if episode.isRecommended == true {
recommendedButton.setImage(#imageLiteral(resourceName: "heart_icon_selected"), for: .normal)
}
- /*
- if episode.isPlaying == true {
- playButton.setImage(#imageLiteral(resourceName: "play_feed_icon_selected"), for: .normal)
- playLabel.text = "Now Playing"
- }
- */
cardID = episode.id
}

diff:
@@ -23,6 +23,7 @@ class FeedViewController: UIViewController, UITableViewDelegate, UITableViewData
///
var feedTableView: UITableView!
var cards: [Card] = []
+ var currentlyPlayingIndexPath: IndexPath?
override func viewDidLoad() {
super.viewDidLoad()
@@ -51,8 +52,16 @@ class FeedViewController: UIViewController, UITableViewDelegate, UITableViewData
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
- feedTableView.reloadData()
+ // check before reloading data whether the Player has stopped playing the currentlyPlayingIndexPath
+ if let indexPath = currentlyPlayingIndexPath {
+ if let card = cards[indexPath.row] as? EpisodeCard {
+ if Player.sharedInstance.currentEpisode?.id != card.episode.id {
+ currentlyPlayingIndexPath = nil
+ }
+ }
+ }
+ feedTableView.reloadData()
}
override func didReceiveMemoryWarning() {
@@ -74,6 +83,9 @@ class FeedViewController: UIViewController, UITableViewDelegate, UITableViewData
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CardTableViewCellIdentifier") as? CardTableViewCell else { return UITableViewCell() }
cell.delegate = self
cell.setupWithCard(card: cards[indexPath.row])
+ if indexPath == currentlyPlayingIndexPath {
+ cell.setPlayButtonToState(isPlaying: true)
+ }
return cell
}
@@ -108,8 +120,18 @@ class FeedViewController: UIViewController, UITableViewDelegate, UITableViewData
func cardTableViewCellDidPressPlayPauseButton(cardTableViewCell: CardTableViewCell) {
guard let cardIndexPath = feedTableView.indexPath(for: cardTableViewCell), let card = cards[cardIndexPath.row] as? EpisodeCard else { return }
- //card.isPlaying = !card.isPlaying
- //cardTableViewCell.setPlayButtonToState(isPlaying: card.isPlaying)
+ if cardIndexPath == currentlyPlayingIndexPath { return }
+
+ if let indexPath = currentlyPlayingIndexPath {
+ if let cell = feedTableView.cellForRow(at: indexPath) as? CardTableViewCell {
+ cell.setPlayButtonToState(isPlaying: false)
+ }
+ }
+ currentlyPlayingIndexPath = cardIndexPath
+ cardTableViewCell.setPlayButtonToState(isPlaying: true)
+ let appDelegate = UIApplication.shared.delegate as! AppDelegate
+ appDelegate.showPlayer(animated: true)
+ Player.sharedInstance.playEpisode(episode: card.episode)
}
func cardTableViewCellDidPressTagButton(cardTableViewCell: CardTableViewCell, index: Int) {